[WIP] Bug 1232439 (972193) - Add FolderTreeView for choosing a folder from a list. draft
authorTom Klein <twointofive@gmail.com>
Sun, 25 Dec 2016 17:47:29 -0600
changeset 453954 2a7c251816cd42ae8eb877649fc9ac5532b2c6f3
parent 453953 9f72271a7109c0be70f338df61b9bcbe2b2294da
child 453955 4ee3eec47f64074dbb75a0745e291303709bd649
child 453959 7f4641fd4743840368d56320e99739d5dd066591
push id39776
push userbmo:twointofive@gmail.com
push dateMon, 26 Dec 2016 21:15:51 +0000
bugs1232439, 972193
milestone53.0a1
[WIP] Bug 1232439 (972193) - Add FolderTreeView for choosing a folder from a list. MozReview-Commit-ID: 2Hbjfok6tqd
mobile/android/base/java/org/mozilla/gecko/FolderTreeItemDecoration.java
mobile/android/base/java/org/mozilla/gecko/FolderTreeView.java
mobile/android/base/java/org/mozilla/gecko/FolderTreeViewAdapter.java
mobile/android/base/locales/en-US/android_strings.dtd
mobile/android/base/moz.build
mobile/android/base/resources/layout/folder_item_view.xml
mobile/android/base/resources/layout/folder_tree_view.xml
mobile/android/base/resources/values/dimens.xml
mobile/android/base/strings.xml.in
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/FolderTreeItemDecoration.java
@@ -0,0 +1,63 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Rect;
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+
+class FolderTreeItemDecoration extends RecyclerView.ItemDecoration {
+    // How far we indent each folder level.
+    private static int indentWidth;
+    // The min amount of room we guarantee for displaying each folder.
+    private static int minFolderWidth;
+    private static int verticalPadding;
+
+    private final int maxFolderDepth;
+
+    private static void initializeDimensions(Context context) {
+        // None of these resources depend on orientation.
+        if (indentWidth == 0) {
+            final Resources resources = context.getResources();
+            indentWidth = resources.getDimensionPixelSize(R.dimen.bookmark_folder_item_indent_width);
+            minFolderWidth = resources.getDimensionPixelSize(R.dimen.bookmark_folder_item_min_content_width);
+            verticalPadding = resources.getDimensionPixelSize(R.dimen.bookmark_folder_item_vpadding);
+        }
+    }
+
+    /* package */ FolderTreeItemDecoration(Context context, int maxFolderDepth) {
+        initializeDimensions(context);
+        this.maxFolderDepth = maxFolderDepth;
+    }
+
+    /* package */ static int computeMaxFolderDepthForWidth(Context context, int availableWidth) {
+        initializeDimensions(context);
+
+        // Compute maxFolderDepth to be the maximum number of indents we can do while still
+        // guaranteeing there will be at least a minimum amount of room (minFolderWidth) for
+        // display of folder names.
+
+        final int availableIndentingWidth = Math.max(availableWidth - minFolderWidth, 0);
+        return availableIndentingWidth / indentWidth;
+    }
+
+    /* package */ int maxFolderDepth() {
+        return maxFolderDepth;
+    }
+
+    @Override
+    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
+        final int position = parent.getChildAdapterPosition(view);
+        if (position == RecyclerView.NO_POSITION) {
+            return;
+        }
+
+        final int folderDepth = Math.min(((FolderTreeView) parent).folderDepth(position), maxFolderDepth);
+        outRect.set(indentWidth * folderDepth, verticalPadding, 0, verticalPadding);
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/FolderTreeView.java
@@ -0,0 +1,86 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko;
+
+import org.mozilla.gecko.db.BrowserDB;
+import org.mozilla.gecko.widget.RecyclerViewClickSupport;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.util.AttributeSet;
+import android.view.View;
+
+class FolderTreeView extends RecyclerView
+                            implements RecyclerViewClickSupport.OnItemClickListener {
+    public interface FolderSelectedListener {
+        void folderSelected(String folderName, int folderId);
+    }
+
+    private FolderTreeItemDecoration itemDecoration;
+
+    private final FolderTreeViewAdapter adapter;
+
+    private FolderSelectedListener folderSelectedListener;
+
+    public FolderTreeView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+
+        setLayoutManager(new LinearLayoutManager(context));
+
+        adapter = new FolderTreeViewAdapter(context);
+        setAdapter(adapter);
+
+        RecyclerViewClickSupport.addTo(this).setOnItemClickListener(this);
+    }
+
+    public void setFolderTree(@NonNull BrowserDB.BookmarkFolderTree folderTree, int selectedFolderId) {
+        adapter.setFolderTree(folderTree);
+        adapter.setSelectedFolder(selectedFolderId);
+    }
+
+    public void setFolderSelectedListener(FolderSelectedListener listener) {
+        folderSelectedListener = listener;
+    }
+
+    @Override
+    public void onItemClicked(RecyclerView recyclerView, int position, View v) {
+        if (folderSelectedListener != null) {
+            final int folderId = adapter.folderId(position);
+            // The "Desktop" folder has special folder id -1; we discard any clicks on it.
+            if (folderId >= 0) {
+                folderSelectedListener.folderSelected(adapter.folderName(position), folderId);
+            }
+        }
+    }
+
+    public int folderDepth(int position) {
+        return adapter.folderDepth(position);
+    }
+
+    @Override
+    public void onSizeChanged(int w, int h, int oldw, int oldh) {
+        if (w == oldw) {
+            return;
+        }
+
+        final int availableWidth = w - getPaddingLeft() - getPaddingRight();
+        final int maxFolderDepthForWidth = FolderTreeItemDecoration.computeMaxFolderDepthForWidth(getContext(), availableWidth);
+
+        if (itemDecoration == null || itemDecoration.maxFolderDepth() != maxFolderDepthForWidth) {
+            // It's possible that some, but not all, item decorations have been computed at this
+            // point, and Android doesn't recompute item decorations as width changes during a
+            // given display event (possible only on the initial show?), so remove and then reset
+            // the item decoration to make sure all items use the correct decoration.
+            if (itemDecoration != null) {
+                removeItemDecoration(itemDecoration);
+            }
+            itemDecoration = new FolderTreeItemDecoration(getContext(), maxFolderDepthForWidth);
+            addItemDecoration(itemDecoration);
+        }
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/FolderTreeViewAdapter.java
@@ -0,0 +1,144 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko;
+
+import org.mozilla.gecko.db.BrowserDB;
+
+import android.content.Context;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.NonNull;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+
+class FolderTreeViewAdapter extends RecyclerView.Adapter<FolderTreeViewAdapter.FolderViewHolder> {
+    private final LayoutInflater inflater;
+
+    private int selectedFolderId;
+
+    private @NonNull ArrayList<Folder> folders;
+
+    private final @DrawableRes int selectableBackgroundId;
+
+    private static class Folder {
+        final int id;
+        final String name;
+        final int depth;
+
+        Folder(int id, String name, int depth) {
+            this.id = id;
+            this.name = name;
+            this.depth = depth;
+        }
+    }
+
+    class FolderViewHolder extends RecyclerView.ViewHolder {
+        private final TextView folderName;
+        private final ImageView folderSelected;
+        private final ImageView folderIcon;
+
+        FolderViewHolder(View view) {
+            super(view);
+            folderName = (TextView) view.findViewById(R.id.folder_name);
+            folderSelected = (ImageView) view.findViewById(R.id.folder_selected);
+            folderIcon = (ImageView) view.findViewById(R.id.folder_icon);
+        }
+
+        void setFolderName(String name) {
+            folderName.setText(name);
+        }
+
+        void setFolderSelected(boolean selected) {
+            folderSelected.setVisibility(selected ? View.VISIBLE : View.GONE);
+        }
+
+        void setFolderClickable(boolean clickable) {
+            folderIcon.setVisibility(clickable ? View.VISIBLE : View.GONE);
+            // Calling itemView.setClickable and/or itemView.setFocusable has no effect here as far
+            // as I can tell.
+            itemView.setBackgroundResource(clickable ? selectableBackgroundId : 0);
+        }
+    }
+
+    FolderTreeViewAdapter(Context context) {
+        inflater = LayoutInflater.from(context);
+
+        folders = new ArrayList<>(0);
+
+        final TypedValue outValue = new TypedValue();
+        context.getTheme().resolveAttribute(android.R.attr.selectableItemBackground, outValue, true);
+        selectableBackgroundId = outValue.resourceId;
+
+    }
+
+    void setFolderTree(@NonNull BrowserDB.BookmarkFolderTree folderTree) {
+        folders = new ArrayList<>();
+        // The root of the tree doesn't correspond to any actual folder, so we start the depth at
+        // -1 so that the first level of actual folders has depth 0.
+        flattenFolderTree(folderTree, folders, -1);
+    }
+
+    /** Adds folders to {@code foldersList} as the tree is traversed. */
+    private static void flattenFolderTree(BrowserDB.BookmarkFolderTree folderTree,
+                                          ArrayList<Folder> foldersList, int currentDepth) {
+        if (folderTree == null) {
+            return;
+        }
+
+        if (currentDepth >= 0) {
+            foldersList.add(new Folder(folderTree.id, folderTree.name, currentDepth));
+        }
+        final int newDepth = currentDepth + 1;
+        for (BrowserDB.BookmarkFolderTree f : folderTree.children.values()) {
+            flattenFolderTree(f, foldersList, newDepth);
+        }
+    }
+
+    /* package */ void setSelectedFolder(int selectedFolderId) {
+        this.selectedFolderId = selectedFolderId;
+        // Rebind to update the selected folder on screen.
+        notifyDataSetChanged();
+    }
+
+    /* package */ int folderDepth(int position) {
+        return folders.get(position).depth;
+    }
+
+    /* package */ int folderId(int position) {
+        return folders.get(position).id;
+    }
+
+    /* package */ String folderName(int position) {
+        return folders.get(position).name;
+    }
+
+    @Override
+    public int getItemCount() {
+        return folders.size();
+    }
+
+    @Override
+    public FolderViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+        final View view = inflater.inflate(R.layout.folder_item_view, parent, false);
+        return new FolderViewHolder(view);
+    }
+
+    @Override
+    public void onBindViewHolder(FolderViewHolder viewHolder, int position) {
+        final Folder folder = folders.get(position);
+        viewHolder.setFolderName(folder.name);
+        viewHolder.setFolderSelected(folder.id == selectedFolderId);
+        viewHolder.setFolderClickable(folder.id >= 0);
+    }
+}
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -70,16 +70,19 @@
 <!ENTITY bookmark_added "Bookmark added">
 <!-- Localization note (bookmark_already_added) : This string is
      used as a label in a toast. It is the verb "to bookmark", not
      the noun "a bookmark". -->
 <!ENTITY bookmark_already_added "Already bookmarked">
 <!ENTITY bookmark_removed "Bookmark removed">
 <!ENTITY bookmark_updated "Bookmark updated">
 <!ENTITY bookmark_options "Options">
+<!-- Localization note (selected_folder): Indicates which Bookmarks folder is currently selected;
+     selected is an adjective and not a verb here. -->
+<!ENTITY bookmarks_selected_folder "Selected folder">
 <!ENTITY screenshot_added_to_bookmarks "Screenshot added to bookmarks">
 <!-- Localization note (screenshot_folder_label_in_bookmarks): We save links to screenshots
      the user takes. The folder we store these links in is located in the bookmarks list
      and is labeled by this String. -->
 <!ENTITY screenshot_folder_label_in_bookmarks "Screenshots">
 <!ENTITY readinglist_smartfolder_label_in_bookmarks "Reading List">
 
 <!-- Localization note (bookmark_folder_items): The variable is replaced by the number of items
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -435,16 +435,19 @@ gbjar.sources += ['java/org/mozilla/geck
     'firstrun/DataPanel.java',
     'firstrun/FirstrunAnimationContainer.java',
     'firstrun/FirstrunPager.java',
     'firstrun/FirstrunPagerConfig.java',
     'firstrun/FirstrunPanel.java',
     'firstrun/RestrictedWelcomePanel.java',
     'firstrun/SyncPanel.java',
     'firstrun/TabQueuePanel.java',
+    'FolderTreeItemDecoration.java',
+    'FolderTreeView.java',
+    'FolderTreeViewAdapter.java',
     'FormAssistPopup.java',
     'GeckoActivity.java',
     'GeckoActivityStatus.java',
     'GeckoApp.java',
     'GeckoApplication.java',
     'GeckoJavaSampler.java',
     'GeckoMessageReceiver.java',
     'GeckoProfilesProvider.java',
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/layout/folder_item_view.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:orientation="horizontal"
+              android:layout_width="match_parent"
+              android:layout_height="wrap_content"
+              android:background="?android:attr/selectableItemBackground"
+              android:clickable="true"
+              android:focusable="true">
+
+        <ImageView android:layout_height="match_parent"
+                   android:layout_width="wrap_content"
+                   android:id="@+id/folder_icon"
+                   android:scaleType="fitCenter"
+                   android:src="@drawable/folder_closed"
+                   android:paddingTop="2dp"
+                   android:paddingRight="6dp"/>
+
+        <TextView android:layout_width="0dp"
+                  android:layout_weight="1"
+                  android:layout_height="wrap_content"
+                  android:id="@+id/folder_name"
+                  android:textSize="14sp"
+                  android:gravity="start|center_vertical"
+                  android:maxLines="1"
+                  android:ellipsize="end"/>
+
+        <ImageView android:layout_height="match_parent"
+                   android:layout_width="wrap_content"
+                   android:id="@+id/folder_selected"
+                   android:scaleType="fitCenter"
+                   android:src="@drawable/img_check"
+                   android:contentDescription="@string/bookmarks_selected_folder"/>
+    </LinearLayout>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/layout/folder_tree_view.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<org.mozilla.gecko.FolderTreeView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:paddingLeft="24dp"
+    android:paddingRight="24dp"
+    android:id="@+id/folder_chooser"/>
--- a/mobile/android/base/resources/values/dimens.xml
+++ b/mobile/android/base/resources/values/dimens.xml
@@ -213,16 +213,20 @@
     <dimen name="find_in_page_status_margin_end">10dip</dimen>
     <dimen name="find_in_page_control_margin_top">2dip</dimen>
     <dimen name="progress_bar_scroll_offset">1.5dp</dimen>
 
     <!-- Matches the built-in divider height. fwiw, in the framework
          I suspect this is a drawable rather than a dimen.  -->
     <dimen name="action_bar_divider_height">2dp</dimen>
 
+    <dimen name="bookmark_folder_item_vpadding">6dp</dimen>
+    <dimen name="bookmark_folder_item_indent_width">30dp</dimen>
+    <dimen name="bookmark_folder_item_min_content_width">120sp</dimen>
+
     <!-- http://blog.danlew.net/2015/01/06/handling-android-resources-with-non-standard-formats/ -->
     <item name="match_parent" type="dimen">-1</item>
     <item name="wrap_content" type="dimen">-2</item>
 
     <item name="tab_strip_content_start" type="dimen">12dp</item>
     <item name="firstrun_tab_strip_content_start" type="dimen">15dp</item>
 
     <item name="notification_media_cover" type="dimen">128dp</item>
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -92,16 +92,17 @@
   <string name="quit">&quit;</string>
   <string name="bookmark">&bookmark;</string>
   <string name="bookmark_remove">&bookmark_remove;</string>
   <string name="bookmark_added">&bookmark_added;</string>
   <string name="bookmark_already_added">&bookmark_already_added;</string>
   <string name="bookmark_removed">&bookmark_removed;</string>
   <string name="bookmark_updated">&bookmark_updated;</string>
   <string name="bookmark_options">&bookmark_options;</string>
+  <string name="bookmarks_selected_folder">&bookmarks_selected_folder;</string>
   <string name="screenshot_added_to_bookmarks">&screenshot_added_to_bookmarks;</string>
   <string name="screenshot_folder_label_in_bookmarks">&screenshot_folder_label_in_bookmarks;</string>
   <string name="readinglist_smartfolder_label_in_bookmarks">&readinglist_smartfolder_label_in_bookmarks;</string>
   <string name="bookmark_folder_items">&bookmark_folder_items;</string>
   <string name="bookmark_folder_one_item">&bookmark_folder_one_item;</string>
 
   <string name="reader_saved_offline">&reader_saved_offline;</string>
   <string name="reader_switch_to_bookmarks">&reader_switch_to_bookmarks;</string>