Bug 1202861 - 1. Create compact (two column) tabs tray option for portrait mode phones. r=sebastian
authorTom Klein <twointofive@gmail.com>
Tue, 29 Nov 2016 13:22:37 -0600
changeset 325433 4de97a01678bc55bf39daea37fd487ec52e5714c
parent 325432 fd3076e08f1147d1e71b8052bc5d9b6ef07df267
child 325434 88e0fb654a104419dd66774d422b20692639919e
push id24
push usermaklebus@msu.edu
push dateTue, 20 Dec 2016 03:11:33 +0000
reviewerssebastian
bugs1202861
milestone53.0a1
Bug 1202861 - 1. Create compact (two column) tabs tray option for portrait mode phones. r=sebastian Changing the setting currently won't take effect until you rotate the tabs panel (to cause it to be recreated); that will be fixed in the next commit. MozReview-Commit-ID: HZfQRy8zubV
mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferences.java
mobile/android/base/java/org/mozilla/gecko/tabs/AutoFitTabsGridLayout.java
mobile/android/base/java/org/mozilla/gecko/tabs/CompactTabsGridLayout.java
mobile/android/base/java/org/mozilla/gecko/tabs/TabsGridLayout.java
mobile/android/base/java/org/mozilla/gecko/tabs/TabsPanel.java
mobile/android/base/locales/en-US/android_strings.dtd
mobile/android/base/moz.build
mobile/android/base/resources/xml/preferences_general.xml
mobile/android/base/strings.xml.in
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferences.java
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferences.java
@@ -161,16 +161,17 @@ OnSharedPreferenceChangeListener
     public static final String PREFS_NOTIFICATIONS_CONTENT_LEARN_MORE = NON_PREF_PREFIX + "notifications.content.learn_more";
     public static final String PREFS_NOTIFICATIONS_WHATS_NEW = NON_PREF_PREFIX + "notifications.whats_new";
     public static final String PREFS_APP_UPDATE_LAST_BUILD_ID = "app.update.last_build_id";
     public static final String PREFS_READ_PARTNER_CUSTOMIZATIONS_PROVIDER = NON_PREF_PREFIX + "distribution.read_partner_customizations_provider";
     public static final String PREFS_READ_PARTNER_BOOKMARKS_PROVIDER = NON_PREF_PREFIX + "distribution.read_partner_bookmarks_provider";
     public static final String PREFS_CUSTOM_TABS = NON_PREF_PREFIX + "customtabs";
     public static final String PREFS_ACTIVITY_STREAM = NON_PREF_PREFIX + "activitystream";
     public static final String PREFS_CATEGORY_EXPERIMENTAL_FEATURES = NON_PREF_PREFIX + "category_experimental";
+    public static final String PREFS_COMPACT_TABS = NON_PREF_PREFIX + "compact_tabs";
 
     private static final String ACTION_STUMBLER_UPLOAD_PREF = "STUMBLER_PREF";
 
 
     // This isn't a Gecko pref, even if it looks like one.
     private static final String PREFS_BROWSER_LOCALE = "locale";
 
     public static final String PREFS_RESTORE_SESSION = NON_PREF_PREFIX + "restoreSession3";
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/AutoFitTabsGridLayout.java
@@ -0,0 +1,95 @@
+/* -*- 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.tabs;
+
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.widget.GridSpacingDecoration;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.support.v7.widget.GridLayoutManager;
+import android.util.AttributeSet;
+import android.util.Log;
+
+class AutoFitTabsGridLayout extends TabsGridLayout {
+    private static final String LOGTAG = "Gecko" + AutoFitTabsGridLayout.class.getSimpleName();
+
+    private GridSpacingDecoration spacingDecoration;
+    private final int desiredItemWidth;
+    private final int desiredHorizontalItemSpacing;
+    private final int minHorizontalItemSpacing;
+    private final int verticalItemPadding;
+
+    AutoFitTabsGridLayout(Context context, AttributeSet attrs) {
+        super(context, attrs, 1);
+
+        final Resources resources = context.getResources();
+        desiredItemWidth = resources.getDimensionPixelSize(R.dimen.tab_panel_item_width);
+        desiredHorizontalItemSpacing = resources.getDimensionPixelSize(R.dimen.tab_panel_grid_ideal_item_hspacing);
+        minHorizontalItemSpacing = resources.getDimensionPixelOffset(R.dimen.tab_panel_grid_min_item_hspacing);
+        verticalItemPadding = resources.getDimensionPixelSize(R.dimen.tab_panel_grid_item_vpadding);
+        final int viewPaddingVertical = resources.getDimensionPixelSize(R.dimen.tab_panel_grid_vpadding);
+        final int viewPaddingHorizontal = resources.getDimensionPixelSize(R.dimen.tab_panel_grid_hpadding);
+
+        setPadding(viewPaddingHorizontal, viewPaddingVertical, viewPaddingHorizontal, viewPaddingVertical);
+    }
+
+    private void updateSpacingDecoration(int horizontalItemSpacing) {
+        if (spacingDecoration != null) {
+            removeItemDecoration(spacingDecoration);
+        }
+        spacingDecoration = new GridSpacingDecoration(horizontalItemSpacing, verticalItemPadding);
+        addItemDecoration(spacingDecoration);
+        updateSelectedPosition();
+    }
+
+    @Override
+    public void onSizeChanged(int w, int h, int oldw, int oldh) {
+        super.onSizeChanged(w, h, oldw, oldh);
+
+        if (w == oldw) {
+            return;
+        }
+
+        final GridLayoutManager layoutManager = (GridLayoutManager) getLayoutManager();
+
+        final int nonPaddingWidth = w - getPaddingLeft() - getPaddingRight();
+        // We lay out the tabs so that the outer two tab edges are butted up against the
+        // RecyclerView padding, and then all other tab edges get their own padding, so
+        // nonPaddingWidth in terms of tab width w and tab spacing s for n tabs is
+        //   n * w + (n - 1) * s
+        // Solving for n gives the formulas below.
+        final int idealSpacingSpanCount = Math.max(1,
+                (nonPaddingWidth + desiredHorizontalItemSpacing) / (desiredItemWidth + desiredHorizontalItemSpacing));
+        final int maxSpanCount = Math.max(1,
+                (nonPaddingWidth + minHorizontalItemSpacing) / (desiredItemWidth + minHorizontalItemSpacing));
+
+        // General caution note: span count can change here at a point where some ItemDecorations
+        // have been computed and some have not, and Android doesn't recompute ItemDecorations after
+        // a setSpanCount call, so we need to always remove and then add back our spacingDecoration
+        // (whose computations depend on spanCount) in order to get a full layout recompute.
+        if (idealSpacingSpanCount == maxSpanCount) {
+            layoutManager.setSpanCount(idealSpacingSpanCount);
+            updateSpacingDecoration(desiredHorizontalItemSpacing);
+        } else {
+            // We're gaining a column by decreasing the item spacing - this actually turns out to be
+            // necessary to fit three columns in landscape mode on many phones.  It also allows us
+            // to match the span counts produced by the previous GridLayout implementation.
+            layoutManager.setSpanCount(maxSpanCount);
+
+            // Increase the spacing as much as we can without giving up our increased span count.
+            for (int spacing = minHorizontalItemSpacing + 1; spacing <= desiredHorizontalItemSpacing; spacing++) {
+                if (maxSpanCount * desiredItemWidth + (maxSpanCount - 1) * spacing > nonPaddingWidth) {
+                    updateSpacingDecoration(spacing - 1);
+                    return;
+                }
+            }
+            // We should never get here if our calculations above were correct.
+            Log.e(LOGTAG, "Span count calculation error");
+            updateSpacingDecoration(minHorizontalItemSpacing);
+        }
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/CompactTabsGridLayout.java
@@ -0,0 +1,27 @@
+/* -*- 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.tabs;
+
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.widget.GridSpacingDecoration;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.util.AttributeSet;
+
+class CompactTabsGridLayout extends TabsGridLayout {
+    CompactTabsGridLayout(Context context, AttributeSet attrs) {
+        super(context, attrs, 2);
+
+        final Resources resources = context.getResources();
+        final int desiredHorizontalItemSpacing = resources.getDimensionPixelSize(R.dimen.tab_panel_grid_ideal_item_hspacing);
+        final int verticalItemPadding = resources.getDimensionPixelSize(R.dimen.tab_panel_grid_item_vpadding);
+        final int viewPaddingVertical = resources.getDimensionPixelSize(R.dimen.tab_panel_grid_vpadding);
+
+        setPadding(desiredHorizontalItemSpacing, viewPaddingVertical, desiredHorizontalItemSpacing, viewPaddingVertical);
+        addItemDecoration(new GridSpacingDecoration(desiredHorizontalItemSpacing, verticalItemPadding));
+    }
+}
--- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsGridLayout.java
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsGridLayout.java
@@ -1,50 +1,28 @@
 /* -*- 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.tabs;
 
 import org.mozilla.gecko.R;
-import org.mozilla.gecko.widget.GridSpacingDecoration;
 
 import android.content.Context;
-import android.content.res.Resources;
 import android.support.v7.widget.GridLayoutManager;
 import android.support.v7.widget.helper.ItemTouchHelper;
 import android.util.AttributeSet;
-import android.util.Log;
 
-public class TabsGridLayout extends TabsLayout {
-    private static final String LOGTAG = "Gecko" + TabsGridLayout.class.getSimpleName();
-
-    private GridSpacingDecoration spacingDecoration;
-    private final int desiredItemWidth;
-    private final int desiredHorizontalItemSpacing;
-    private final int minHorizontalItemSpacing;
-    private final int verticalItemPadding;
-
-    public TabsGridLayout(Context context, AttributeSet attrs) {
+abstract class TabsGridLayout extends TabsLayout {
+    TabsGridLayout(Context context, AttributeSet attrs, int spanCount) {
         super(context, attrs, R.layout.tabs_layout_item_view);
 
-        final Resources resources = context.getResources();
-
-        // Actual span count is updated in onSizeChanged.
-        setLayoutManager(new GridLayoutManager(context, 1));
+        setLayoutManager(new GridLayoutManager(context, spanCount));
 
-        desiredItemWidth = resources.getDimensionPixelSize(R.dimen.tab_panel_item_width);
-        desiredHorizontalItemSpacing = resources.getDimensionPixelSize(R.dimen.tab_panel_grid_ideal_item_hspacing);
-        minHorizontalItemSpacing = resources.getDimensionPixelOffset(R.dimen.tab_panel_grid_min_item_hspacing);
-        verticalItemPadding = resources.getDimensionPixelSize(R.dimen.tab_panel_grid_item_vpadding);
-        final int viewPaddingHorizontal = resources.getDimensionPixelSize(R.dimen.tab_panel_grid_hpadding);
-        final int viewPaddingVertical = resources.getDimensionPixelSize(R.dimen.tab_panel_grid_vpadding);
-
-        setPadding(viewPaddingHorizontal, viewPaddingVertical, viewPaddingHorizontal, viewPaddingVertical);
         setClipToPadding(false);
         setScrollBarStyle(SCROLLBARS_OUTSIDE_OVERLAY);
 
         setItemAnimator(new TabsGridLayoutAnimator());
 
         // A TouchHelper handler for swipe to close.
         final TabsTouchHelperCallback callback = new TabsTouchHelperCallback(this) {
             @Override
@@ -70,65 +48,9 @@ public class TabsGridLayout extends Tabs
         final int firstVisibleIndex = layoutManager.findFirstVisibleItemPosition();
         // When you add an item at the first visible position to a GridLayoutManager and there's
         // room to scroll, RecyclerView scrolls the new position to anywhere from near the bottom of
         // its row to completely offscreen (for unknown reasons), so we need to scroll to fix that.
         // We also scroll when the item being added is the only item on the final row.
         return index == firstVisibleIndex ||
                 (index == getAdapter().getItemCount() - 1 && index % spanCount == 0);
     }
-
-    private void updateSpacingDecoration(int horizontalItemSpacing) {
-        if (spacingDecoration != null) {
-            removeItemDecoration(spacingDecoration);
-        }
-        spacingDecoration = new GridSpacingDecoration(horizontalItemSpacing, verticalItemPadding);
-        addItemDecoration(spacingDecoration);
-        updateSelectedPosition();
-    }
-
-    @Override
-    public void onSizeChanged(int w, int h, int oldw, int oldh) {
-        super.onSizeChanged(w, h, oldw, oldh);
-
-        if (w == oldw) {
-            return;
-        }
-
-        final GridLayoutManager layoutManager = (GridLayoutManager) getLayoutManager();
-
-        final int nonPaddingWidth = w - getPaddingLeft() - getPaddingRight();
-        // We lay out the tabs so that the outer two tab edges are butted up against the
-        // RecyclerView padding, and then all other tab edges get their own padding, so
-        // nonPaddingWidth in terms of tab width w and tab spacing s for n tabs is
-        //   n * w + (n - 1) * s
-        // Solving for n gives the formulas below.
-        final int idealSpacingSpanCount = Math.max(1,
-                (nonPaddingWidth + desiredHorizontalItemSpacing) / (desiredItemWidth + desiredHorizontalItemSpacing));
-        final int maxSpanCount = Math.max(1,
-                (nonPaddingWidth + minHorizontalItemSpacing) / (desiredItemWidth + minHorizontalItemSpacing));
-
-        // General caution note: span count can change here at a point where some ItemDecorations
-        // have been computed and some have not, and Android doesn't recompute ItemDecorations after
-        // a setSpanCount call, so we need to always remove and then add back our spacingDecoration
-        // (whose computations depend on spanCount) in order to get a full layout recompute.
-        if (idealSpacingSpanCount == maxSpanCount) {
-            layoutManager.setSpanCount(idealSpacingSpanCount);
-            updateSpacingDecoration(desiredHorizontalItemSpacing);
-        } else {
-            // We're gaining a column by decreasing the item spacing - this actually turns out to be
-            // necessary to fit three columns in landscape mode on many phones.  It also allows us
-            // to match the span counts produced by the previous GridLayout implementation.
-            layoutManager.setSpanCount(maxSpanCount);
-
-            // Increase the spacing as much as we can without giving up our increased span count.
-            for (int spacing = minHorizontalItemSpacing + 1; spacing <= desiredHorizontalItemSpacing; spacing++) {
-                if (maxSpanCount * desiredItemWidth + (maxSpanCount - 1) * spacing > nonPaddingWidth) {
-                    updateSpacingDecoration(spacing - 1);
-                    return;
-                }
-            }
-            // We should never get here if our calculations above were correct.
-            Log.e(LOGTAG, "Span count calculation error");
-            updateSpacingDecoration(minHorizontalItemSpacing);
-        }
-    }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsPanel.java
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsPanel.java
@@ -4,23 +4,25 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.tabs;
 
 import android.support.v4.content.ContextCompat;
 import org.mozilla.gecko.AppConstants.Versions;
 import org.mozilla.gecko.GeckoApp;
 import org.mozilla.gecko.GeckoApplication;
+import org.mozilla.gecko.GeckoSharedPrefs;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 import org.mozilla.gecko.animation.PropertyAnimator;
 import org.mozilla.gecko.animation.ViewHelper;
 import org.mozilla.gecko.lwt.LightweightTheme;
 import org.mozilla.gecko.lwt.LightweightThemeDrawable;
+import org.mozilla.gecko.preferences.GeckoPreferences;
 import org.mozilla.gecko.restrictions.Restrictable;
 import org.mozilla.gecko.restrictions.Restrictions;
 import org.mozilla.gecko.util.HardwareUtils;
 import org.mozilla.gecko.widget.GeckoPopupMenu;
 import org.mozilla.gecko.widget.IconTabWidget;
 
 import android.content.Context;
 import android.content.res.Configuration;
@@ -67,19 +69,24 @@ public class TabsPanel extends LinearLay
     public interface TabsLayoutChangeListener {
         void onTabsLayoutChange(int width, int height);
     }
 
     public static View createTabsLayout(final Context context, final AttributeSet attrs) {
         final boolean isLandscape = context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
 
         if (HardwareUtils.isTablet() || isLandscape) {
-            return new TabsGridLayout(context, attrs);
+            return new AutoFitTabsGridLayout(context, attrs);
         } else {
-            return new TabsListLayout(context, attrs);
+            // Phone in portrait mode.
+            if (GeckoSharedPrefs.forApp(context).getBoolean(GeckoPreferences.PREFS_COMPACT_TABS, false)) {
+                return new CompactTabsGridLayout(context, attrs);
+            } else {
+                return new TabsListLayout(context, attrs);
+            }
         }
     }
 
     private final Context mContext;
     private final GeckoApp mActivity;
     private final LightweightTheme mTheme;
     private RelativeLayout mHeader;
     private FrameLayout mTabsContainer;
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -531,16 +531,21 @@ size. -->
 <!-- Localization note (pref_scroll_title_bar2): Label for setting that controls
      whether or not the dynamic toolbar is enabled. -->
 <!ENTITY pref_scroll_title_bar2 "Full-screen browsing">
 <!ENTITY pref_scroll_title_bar_summary2 "Hide the &brandShortName; toolbar when scrolling down a page">
 
 <!ENTITY pref_tab_queue_title3 "Tab queue">
 <!ENTITY pref_tab_queue_summary4 "Save links until the next time you open &brandShortName;">
 
+<!-- Localization note (pref_compact_tabs): Label for setting that controls whether 1 wide column or
+     2 narrower (compact) columns are used for tabs in the tabs tray in portrait mode on phones. -->
+<!ENTITY pref_compact_tabs "Compact tabs">
+<!ENTITY pref_compact_tabs_summary "Display two columns of tabs in the tabs tray in portrait mode">
+
 <!-- Localization note (page_removed): This string appears in a toast message when
      any page is removed frome about:home. This includes pages that are in history,
      bookmarks, or reading list. -->
 <!ENTITY page_removed "Page removed">
 
 <!ENTITY bookmark_edit_title "Edit Bookmark">
 <!ENTITY bookmark_edit_name "Name">
 <!ENTITY bookmark_edit_location "Location">
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -672,16 +672,18 @@ gbjar.sources += ['java/org/mozilla/geck
     'SnackbarBuilder.java',
     'SuggestClient.java',
     'Tab.java',
     'tabqueue/TabQueueHelper.java',
     'tabqueue/TabQueuePrompt.java',
     'tabqueue/TabQueueService.java',
     'tabqueue/TabReceivedService.java',
     'Tabs.java',
+    'tabs/AutoFitTabsGridLayout.java',
+    'tabs/CompactTabsGridLayout.java',
     'tabs/PrivateTabsPanel.java',
     'tabs/TabCurve.java',
     'tabs/TabHistoryController.java',
     'tabs/TabHistoryFragment.java',
     'tabs/TabHistoryItemRow.java',
     'tabs/TabHistoryPage.java',
     'tabs/TabPanelBackButton.java',
     'tabs/TabsGridLayout.java',
--- a/mobile/android/base/resources/xml/preferences_general.xml
+++ b/mobile/android/base/resources/xml/preferences_general.xml
@@ -28,10 +28,17 @@
                       android:title="@string/pref_scroll_title_bar2"
                       android:summary="@string/pref_scroll_title_bar_summary" />
 
     <SwitchPreference android:key="android.not_a_preference.tab_queue"
                       android:title="@string/pref_tab_queue_title"
                       android:summary="@string/pref_tab_queue_summary"
                       android:defaultValue="false" />
 
+    <SwitchPreference android:key="android.not_a_preference.compact_tabs"
+                      android:title="@string/pref_compact_tabs"
+                      android:summary="@string/pref_compact_tabs_summary"
+                      android:defaultValue="false"
+                      android:persistent="true"/>
+
+
 </PreferenceScreen>
 
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -424,16 +424,19 @@
   <string name="doorhanger_login_select_title">&doorhanger_login_select_title;</string>
 
   <string name="pref_magnifying_glass_enabled">&pref_magnifying_glass_enabled;</string>
   <string name="pref_magnifying_glass_enabled_summary">&pref_magnifying_glass_enabled_summary2;</string>
 
   <string name="pref_scroll_title_bar2">&pref_scroll_title_bar2;</string>
   <string name="pref_scroll_title_bar_summary">&pref_scroll_title_bar_summary2;</string>
 
+  <string name="pref_compact_tabs">&pref_compact_tabs;</string>
+  <string name="pref_compact_tabs_summary">&pref_compact_tabs_summary;</string>
+
   <string name="page_removed">&page_removed;</string>
 
   <string name="bookmark_edit_title">&bookmark_edit_title;</string>
   <string name="bookmark_edit_name">&bookmark_edit_name;</string>
   <string name="bookmark_edit_location">&bookmark_edit_location;</string>
   <string name="bookmark_edit_keyword">&bookmark_edit_keyword;</string>
 
   <string name="pref_use_master_password">&pref_use_master_password;</string>