[WIP]
Bug 1232439 (972193) - Add FolderTreeView for choosing a folder from a list.
MozReview-Commit-ID: 2Hbjfok6tqd
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>