Bug 1331154 - 1. Add TabsLayout support for reordering tabs. r?sebastian draft
authorTom Klein <twointofive@gmail.com>
Thu, 02 Feb 2017 17:15:27 -0600
changeset 480038 8793e90dbe8a4d16c5de7bf67c9dbc686ad339a9
parent 479455 12c02bf624c48903b155428f7c8a419ba7a333a6
child 480039 436eee4a52c4a4c4168087487d40a10232435227
push id44433
push userbmo:twointofive@gmail.com
push dateTue, 07 Feb 2017 18:39:01 +0000
reviewerssebastian
bugs1331154
milestone54.0a1
Bug 1331154 - 1. Add TabsLayout support for reordering tabs. r?sebastian Future commits will update the tabs lists in Tabs and gecko; for now we're just updating the TabsLayoutAdapter list. When considering some of the changes in TabsTouchHelperCallback, note that TabStripView uses the new drag and drop, but not swipe to close. Note from the future: Tabs.moveTabInList will eventually be used on the Tabs list in Tabs as well, so placing the method in Tabs seems best. MozReview-Commit-ID: EEseqmVIZmY
mobile/android/base/java/org/mozilla/gecko/Tabs.java
mobile/android/base/java/org/mozilla/gecko/tabs/TabStripAdapter.java
mobile/android/base/java/org/mozilla/gecko/tabs/TabStripView.java
mobile/android/base/java/org/mozilla/gecko/tabs/TabsGridLayout.java
mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayout.java
mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayoutAdapter.java
mobile/android/base/java/org/mozilla/gecko/tabs/TabsListLayout.java
mobile/android/base/java/org/mozilla/gecko/tabs/TabsTouchHelperCallback.java
--- a/mobile/android/base/java/org/mozilla/gecko/Tabs.java
+++ b/mobile/android/base/java/org/mozilla/gecko/Tabs.java
@@ -1042,9 +1042,18 @@ public class Tabs implements BundleEvent
 
     private int getTabColor(Tab tab) {
         if (tab != null) {
             return tab.isPrivate() ? mPrivateClearColor : Color.WHITE;
         }
 
         return Color.WHITE;
     }
+
+    static public void moveTabInList(List<Tab> tabsList, int fromPosition, int toPosition) {
+        final Tab movedTab = tabsList.get(fromPosition);
+        final int step = (fromPosition < toPosition) ? 1 : -1;
+        for (int i = fromPosition; i != toPosition; i += step) {
+            tabsList.set(i, tabsList.get(i + step));
+        }
+        tabsList.set(toPosition, movedTab);
+    }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabStripAdapter.java
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabStripAdapter.java
@@ -10,16 +10,17 @@ import android.support.annotation.NonNul
 import android.support.v7.widget.RecyclerView;
 import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Tab;
+import org.mozilla.gecko.Tabs;
 
 import java.util.ArrayList;
 import java.util.List;
 
 class TabStripAdapter extends RecyclerView.Adapter<TabStripAdapter.TabStripViewHolder> {
     private static final String LOGTAG = "Gecko" + TabStripAdapter.class.getSimpleName();
 
     private @NonNull List<Tab> tabs;
@@ -81,16 +82,22 @@ class TabStripAdapter extends RecyclerVi
     /* package */ void notifyTabChanged(Tab tab) {
         final int position =  getPositionForTab(tab);
         if (position == -1) {
             return;
         }
         notifyItemChanged(position);
     }
 
+    /* package */ boolean moveTab(int fromPosition, int toPosition) {
+        Tabs.moveTabInList(tabs, fromPosition, toPosition);
+        notifyItemMoved(fromPosition, toPosition);
+        return true;
+    }
+
     /* package */ void clear() {
         tabs = new ArrayList<>(0);
         notifyDataSetChanged();
     }
 
     @Override
     public TabStripViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
         final TabStripItemView view = (TabStripItemView) inflater.inflate(R.layout.tab_strip_item, parent, false);
--- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabStripView.java
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabStripView.java
@@ -16,25 +16,27 @@ import android.animation.ObjectAnimator;
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Canvas;
 import android.graphics.LinearGradient;
 import android.graphics.Paint;
 import android.graphics.Shader;
 import android.support.v7.widget.LinearLayoutManager;
 import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.helper.ItemTouchHelper;
 import android.util.AttributeSet;
 import android.view.View;
 import android.view.ViewTreeObserver;
 import android.view.animation.DecelerateInterpolator;
 
 import java.util.ArrayList;
 import java.util.List;
 
-public class TabStripView extends RecyclerView {
+public class TabStripView extends RecyclerView
+                          implements TabsTouchHelperCallback.DragListener {
     private static final int ANIM_TIME_MS = 200;
     private static final DecelerateInterpolator ANIM_INTERPOLATOR = new DecelerateInterpolator();
 
     private final TabStripAdapter adapter;
     private boolean isPrivate;
 
     private final TabAnimatorListener animatorListener;
 
@@ -58,16 +60,22 @@ public class TabStripView extends Recycl
 
         final LinearLayoutManager layoutManager = new LinearLayoutManager(context);
         layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
         setLayoutManager(layoutManager);
 
         setItemAnimator(new TabStripItemAnimator(ANIM_TIME_MS));
 
         addItemDecoration(new TabStripDividerItem(context));
+
+        final int dragDirections = ItemTouchHelper.START | ItemTouchHelper.END;
+        // A TouchHelper handler for drag and drop.
+        final TabsTouchHelperCallback callback = new TabsTouchHelperCallback(this, dragDirections);
+        final ItemTouchHelper touchHelper = new ItemTouchHelper(callback);
+        touchHelper.attachToRecyclerView(this);
     }
 
     /* package */ void refreshTabs() {
         // Store a different copy of the tabs, so that we don't have
         // to worry about accidentally updating it on the wrong thread.
         final List<Tab> tabs = new ArrayList<>();
 
         for (final Tab tab : Tabs.getInstance().getTabsInOrder()) {
@@ -121,16 +129,21 @@ public class TabStripView extends Recycl
         }
 
     }
 
     /* package */ void updateTab(Tab tab) {
         adapter.notifyTabChanged(tab);
     }
 
+    @Override
+    public boolean onItemMove(int fromPosition, int toPosition) {
+        return adapter.moveTab(fromPosition, toPosition);
+    }
+
     public int getPositionForSelectedTab() {
         return adapter.getPositionForTab(Tabs.getInstance().getSelectedTab());
     }
 
     private void updateSelectedPosition() {
         final int selected = getPositionForSelectedTab();
         if (selected != -1) {
             scrollToPosition(selected);
--- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsGridLayout.java
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsGridLayout.java
@@ -18,18 +18,19 @@ abstract class TabsGridLayout extends Ta
 
         setLayoutManager(new GridLayoutManager(context, spanCount));
 
         setClipToPadding(false);
         setScrollBarStyle(SCROLLBARS_OUTSIDE_OVERLAY);
 
         setItemAnimator(new TabsGridLayoutAnimator());
 
-        // A TouchHelper handler for swipe to close.
-        final TabsTouchHelperCallback callback = new TabsTouchHelperCallback(this) {
+        final int dragDirections = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.START | ItemTouchHelper.END;
+        // A TouchHelper handler for drag and drop and swipe to close.
+        final TabsTouchHelperCallback callback = new TabsTouchHelperCallback(this, dragDirections, this) {
             @Override
             protected float alphaForItemSwipeDx(float dX, int distanceToAlphaMin) {
                 return 1f - 2f * Math.abs(dX) / distanceToAlphaMin;
             }
         };
         final ItemTouchHelper touchHelper = new ItemTouchHelper(callback);
         touchHelper.attachToRecyclerView(this);
     }
--- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayout.java
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayout.java
@@ -19,17 +19,18 @@ import android.view.View;
 import android.widget.Button;
 
 import java.util.ArrayList;
 
 public abstract class TabsLayout extends RecyclerView
         implements TabsPanel.TabsLayout,
         Tabs.OnTabsChangedListener,
         RecyclerViewClickSupport.OnItemClickListener,
-        TabsTouchHelperCallback.DismissListener {
+        TabsTouchHelperCallback.DismissListener,
+        TabsTouchHelperCallback.DragListener {
 
     private static final String LOGTAG = "Gecko" + TabsLayout.class.getSimpleName();
 
     private final boolean isPrivate;
     private TabsPanel tabsPanel;
     private final TabsLayoutAdapter tabsAdapter;
 
     public TabsLayout(Context context, AttributeSet attrs, int itemViewLayoutResId) {
@@ -144,16 +145,26 @@ public abstract class TabsLayout extends
             // tap on a tab while its remove animation is running), so ignore the click.
             return;
         }
 
         autoHidePanel();
         Tabs.getInstance().notifyListeners(tab, Tabs.TabEvents.OPENED_FROM_TABS_TRAY);
     }
 
+    @Override
+    public void onItemDismiss(View view) {
+        closeTab(view);
+    }
+
+    @Override
+    public boolean onItemMove(int fromPosition, int toPosition) {
+        return tabsAdapter.moveTab(fromPosition, toPosition);
+    }
+
     /** Updates the selected position in the list so that it will be scrolled to the right place. */
     protected void updateSelectedPosition() {
         final int selected = getSelectedAdapterPosition();
         if (selected != NO_POSITION) {
             scrollToPosition(selected);
         }
     }
 
@@ -196,21 +207,16 @@ public abstract class TabsLayout extends
             // but in the private panel we only want to close private tabs.
             if (!isPrivate || tab.isPrivate()) {
                 Tabs.getInstance().closeTab(tab, false);
             }
         }
     }
 
     @Override
-    public void onItemDismiss(View view) {
-        closeTab(view);
-    }
-
-    @Override
     public void onChildAttachedToWindow(View child) {
         // Make sure we reset any attributes that may have been animated in this child's previous
         // incarnation.
         child.setTranslationX(0);
         child.setTranslationY(0);
         child.setAlpha(1);
     }
 
--- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayoutAdapter.java
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayoutAdapter.java
@@ -1,16 +1,17 @@
 /* -*- 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.Tab;
+import org.mozilla.gecko.Tabs;
 
 import android.content.Context;
 import android.support.annotation.NonNull;
 import android.support.v7.widget.RecyclerView;
 import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -92,16 +93,22 @@ public class TabsLayoutAdapter
             notifyItemInserted(tabs.size() - 1);
             // index == -1 is a valid way to add to the end, the other cases are errors.
             if (index != -1) {
                 Log.e(LOGTAG, "Tab was inserted at an invalid position: " + Integer.toString(index));
             }
         }
     }
 
+    /* package */ boolean moveTab(int fromPosition, int toPosition) {
+        Tabs.moveTabInList(tabs, fromPosition, toPosition);
+        notifyItemMoved(fromPosition, toPosition);
+        return true;
+    }
+
     @Override
     public int getItemCount() {
         return tabs.size();
     }
 
     private Tab getItem(int position) {
         return tabs.get(position);
     }
--- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsListLayout.java
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsListLayout.java
@@ -26,18 +26,19 @@ public class TabsListLayout extends Tabs
 
     public TabsListLayout(Context context, AttributeSet attrs) {
         super(context, attrs, R.layout.tabs_list_item_view);
 
         setHasFixedSize(true);
 
         setLayoutManager(new LinearLayoutManager(context));
 
-        // A TouchHelper handler for swipe to close.
-        final TabsTouchHelperCallback callback = new TabsTouchHelperCallback(this) {
+        final int dragDirections = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
+        // A TouchHelper handler for drag and drop and swipe to close.
+        final TabsTouchHelperCallback callback = new TabsTouchHelperCallback(this, dragDirections, this) {
             @Override
             protected float alphaForItemSwipeDx(float dX, int distanceToAlphaMin) {
                 return Math.max(0.1f,
                         Math.min(1f, 1f - 2f * Math.abs(dX) / distanceToAlphaMin));
             }
         };
         final ItemTouchHelper touchHelper = new ItemTouchHelper(callback);
         touchHelper.attachToRecyclerView(this);
--- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsTouchHelperCallback.java
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsTouchHelperCallback.java
@@ -1,76 +1,101 @@
 /* -*- 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 android.graphics.Canvas;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
 import android.support.v7.widget.RecyclerView;
 import android.support.v7.widget.helper.ItemTouchHelper;
 import android.view.View;
 
-abstract class TabsTouchHelperCallback extends ItemTouchHelper.Callback {
-    private final DismissListener dismissListener;
+class TabsTouchHelperCallback extends ItemTouchHelper.Callback {
+    private final @Nullable DismissListener dismissListener;
+    private final @NonNull DragListener dragListener;
+    private final int movementFlags;
 
     interface DismissListener {
         void onItemDismiss(View view);
     }
 
-    public TabsTouchHelperCallback(DismissListener dismissListener) {
+    interface DragListener {
+        boolean onItemMove(int fromPosition, int toPosition);
+    }
+
+    TabsTouchHelperCallback(@NonNull DragListener dragListener, int dragDirections) {
+        this(dragListener, dragDirections, null);
+    }
+
+    TabsTouchHelperCallback(@NonNull DragListener dragListener, int dragDirections, @Nullable DismissListener dismissListener) {
+        this.dragListener = dragListener;
         this.dismissListener = dismissListener;
+        // Tabs are only ever swiped left or right to dismiss, not up or down.
+        final int swipeDirections = (dismissListener == null) ? 0 : ItemTouchHelper.START | ItemTouchHelper.END;
+        movementFlags = makeMovementFlags(dragDirections, swipeDirections);
     }
 
     @Override
     public boolean isItemViewSwipeEnabled() {
+        return dismissListener != null;
+    }
+
+    @Override
+    public boolean isLongPressDragEnabled() {
         return true;
     }
 
     @Override
     public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
-        return makeFlag(ItemTouchHelper.ACTION_STATE_SWIPE, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT);
+        return movementFlags;
     }
 
     @Override
     public void onSwiped(RecyclerView.ViewHolder viewHolder, int i) {
         dismissListener.onItemDismiss(viewHolder.itemView);
     }
 
     @Override
     public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder,
                           RecyclerView.ViewHolder target) {
-        return false;
+        final int fromPosition = viewHolder.getAdapterPosition();
+        final int toPosition = target.getAdapterPosition();
+        if (fromPosition == RecyclerView.NO_POSITION || toPosition == RecyclerView.NO_POSITION) {
+            return false;
+        }
+        return dragListener.onItemMove(fromPosition, toPosition);
     }
 
     /**
      * Returns the alpha an itemView should be set to when swiped by an amount {@code dX}, given
      * that alpha should decrease to its min at distance {@code distanceToAlphaMin}.
      */
-    abstract protected float alphaForItemSwipeDx(float dX, int distanceToAlphaMin);
+    /* package */ protected float alphaForItemSwipeDx(float dX, int distanceToAlphaMin) {
+        return 1;
+    }
 
-    /**
-     * Alpha on an itemView being swiped should decrease to a min over a distance equal to the
-     * width of the item being swiped.
-     */
     @Override
     public void onChildDraw(Canvas c,
                             RecyclerView recyclerView,
                             RecyclerView.ViewHolder viewHolder,
                             float dX,
                             float dY,
                             int actionState,
                             boolean isCurrentlyActive) {
-        if (actionState != ItemTouchHelper.ACTION_STATE_SWIPE) {
-            return;
-        }
-
         super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
 
-        viewHolder.itemView.setAlpha(alphaForItemSwipeDx(dX, viewHolder.itemView.getWidth()));
+        if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
+            // Alpha on an itemView being swiped should decrease to a min over a distance equal to
+            // the width of the item being swiped.
+            viewHolder.itemView.setAlpha(alphaForItemSwipeDx(dX, viewHolder.itemView.getWidth()));
+        }
     }
 
+    @Override
     public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
         super.clearView(recyclerView, viewHolder);
         viewHolder.itemView.setAlpha(1);
     }
 }