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 370391 4de97a01678bc55bf39daea37fd487ec52e5714c
parent 370390 fd3076e08f1147d1e71b8052bc5d9b6ef07df267
child 370392 88e0fb654a104419dd66774d422b20692639919e
push id6996
push userjlorenzo@mozilla.com
push dateMon, 06 Mar 2017 20:48:21 +0000
treeherdermozilla-beta@d89512dab048 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssebastian
bugs1202861
milestone53.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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>