Bug 1186529 - Add RecyclerViewClickSupport for click handling. r=sebastian
authorJonathan Almeida [:jonalmeida] <jalmeida@mozilla.com>
Wed, 05 Aug 2015 16:53:30 -0700
changeset 288309 c9b41f67d8323d2b542fb468f38fd27b176b9c30
parent 288308 f48f05a347fcfe62d28917896c905eb98c2112a9
child 288310 05f8adacf21fd51a1acf567fd88baea725755ea4
push id5067
push userraliiev@mozilla.com
push dateMon, 21 Sep 2015 14:04:52 +0000
treeherdermozilla-beta@14221ffe5b2f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssebastian
bugs1186529
milestone42.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 1186529 - Add RecyclerViewClickSupport for click handling. r=sebastian
mobile/android/base/home/PanelRecyclerView.java
mobile/android/base/home/RecyclerViewItemClickListener.java
mobile/android/base/home/SearchEngineBar.java
mobile/android/base/moz.build
mobile/android/base/resources/values/ids.xml
mobile/android/base/widget/RecyclerViewClickSupport.java
--- a/mobile/android/base/home/PanelRecyclerView.java
+++ b/mobile/android/base/home/PanelRecyclerView.java
@@ -3,31 +3,34 @@
  * 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.home;
 
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.home.PanelLayout.DatasetBacked;
 import org.mozilla.gecko.home.PanelLayout.PanelView;
-import org.mozilla.gecko.home.RecyclerViewItemClickListener.OnClickListener;
+import org.mozilla.gecko.widget.RecyclerViewClickSupport;
+import org.mozilla.gecko.widget.RecyclerViewClickSupport.OnItemClickListener;
+import org.mozilla.gecko.widget.RecyclerViewClickSupport.OnItemLongClickListener;
 
 import android.annotation.SuppressLint;
 import android.content.Context;
 import android.content.res.Resources;
 import android.database.Cursor;
 import android.support.v7.widget.GridLayoutManager;
 import android.support.v7.widget.RecyclerView;
 import android.view.View;
 
 /**
  * RecyclerView implementation for grid home panels.
  */
 @SuppressLint("ViewConstructor") // View is only created from code
-public class PanelRecyclerView extends RecyclerView implements DatasetBacked, PanelView, OnClickListener {
+public class PanelRecyclerView extends RecyclerView
+        implements DatasetBacked, PanelView, OnItemClickListener, OnItemLongClickListener {
     private final PanelRecyclerViewAdapter adapter;
     private final GridLayoutManager layoutManager;
     private final PanelViewItemHandler itemHandler;
     private final float columnWidth;
     private final boolean autoFit;
     private final HomeConfig.ViewConfig viewConfig;
 
     private PanelLayout.OnItemOpenListener itemOpenListener;
@@ -64,17 +67,19 @@ public class PanelRecyclerView extends R
         int verticalSpacing = (int) resources.getDimension(R.dimen.panel_grid_view_vertical_spacing);
         int outerSpacing = (int) resources.getDimension(R.dimen.panel_grid_view_outer_spacing);
 
         addItemDecoration(new SpacingDecoration(horizontalSpacing, verticalSpacing));
 
         setPadding(outerSpacing, outerSpacing, outerSpacing, outerSpacing);
         setClipToPadding(false);
 
-        addOnItemTouchListener(new RecyclerViewItemClickListener(context, this, this));
+        RecyclerViewClickSupport.addTo(this)
+            .setOnItemClickListener(this)
+            .setOnItemLongClickListener(this);
     }
 
     @Override
     protected void onMeasure(int widthSpec, int heightSpec) {
         super.onMeasure(widthSpec, heightSpec);
 
         if (autoFit) {
             // Adjust span based on space available (What GridView does when you say numColumns="auto_fit")
@@ -118,36 +123,36 @@ public class PanelRecyclerView extends R
     }
 
     @Override
     public void setContextMenuInfoFactory(HomeContextMenuInfo.Factory factory) {
         contextMenuInfoFactory = factory;
     }
 
     @Override
-    public void onClick(View view, int position) {
+    public void onItemClicked(RecyclerView recyclerView, int position, View v) {
         if (viewConfig.hasHeaderConfig()) {
             if (position == 0) {
                 itemOpenListener.onItemOpen(viewConfig.getHeaderConfig().getUrl(), null);
                 return;
             }
 
             position--;
         }
 
         itemHandler.openItemAtPosition(adapter.getCursor(), position);
     }
 
     @Override
-    public void onLongClick(View view, int position) {
+    public boolean onItemLongClicked(RecyclerView recyclerView, int position, View v) {
         Cursor cursor = adapter.getCursor();
         cursor.moveToPosition(position);
 
-        contextMenuInfo = contextMenuInfoFactory.makeInfoForCursor(view, position, -1, cursor);
-        showContextMenuForChild(PanelRecyclerView.this);
+        contextMenuInfo = contextMenuInfoFactory.makeInfoForCursor(recyclerView, position, -1, cursor);
+        return showContextMenuForChild(PanelRecyclerView.this);
     }
 
     private class PanelSpanSizeLookup extends GridLayoutManager.SpanSizeLookup {
         @Override
         public int getSpanSize(int position) {
             if (position == 0 && viewConfig.hasHeaderConfig()) {
                 return layoutManager.getSpanCount();
             }
deleted file mode 100644
--- a/mobile/android/base/home/RecyclerViewItemClickListener.java
+++ /dev/null
@@ -1,87 +0,0 @@
-/* -*- 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.home;
-
-import android.content.Context;
-import android.support.v7.widget.RecyclerView;
-import android.view.GestureDetector;
-import android.view.HapticFeedbackConstants;
-import android.view.MotionEvent;
-import android.view.View;
-
-/**
- * RecyclerView.OnItemTouchListener implementation that will notify an OnClickListener about clicks and long clicks
- * on items displayed by the RecyclerView.
- */
-public class RecyclerViewItemClickListener implements RecyclerView.OnItemTouchListener,
-                                                      GestureDetector.OnGestureListener {
-    public interface OnClickListener {
-        void onClick(View view, int position);
-        void onLongClick(View view, int position);
-    }
-
-    private final OnClickListener clickListener;
-    private final GestureDetector gestureDetector;
-    private final RecyclerView recyclerView;
-
-    public RecyclerViewItemClickListener(Context context, RecyclerView recyclerView, OnClickListener clickListener) {
-        this.clickListener = clickListener;
-        this.gestureDetector = new GestureDetector(context, this);
-        this.recyclerView = recyclerView;
-    }
-
-    @Override
-    public boolean onInterceptTouchEvent(RecyclerView recyclerView, MotionEvent event) {
-        return gestureDetector.onTouchEvent(event);
-    }
-
-    @Override
-    public boolean onSingleTapUp(MotionEvent event) {
-        View childView = recyclerView.findChildViewUnder(event.getX(), event.getY());
-
-        if (childView != null) {
-            final int position = recyclerView.getChildAdapterPosition(childView);
-            clickListener.onClick(childView, position);
-        }
-
-        return true;
-    }
-
-    @Override
-    public void onLongPress(MotionEvent event) {
-        View childView = recyclerView.findChildViewUnder(event.getX(), event.getY());
-
-        if (childView != null) {
-            final int position = recyclerView.getChildAdapterPosition(childView);
-            childView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
-            clickListener.onLongClick(childView, position);
-        }
-    }
-
-    @Override
-    public boolean onDown(MotionEvent e) {
-        return false;
-    }
-
-    @Override
-    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
-        return false;
-    }
-
-    @Override
-    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
-        return false;
-    }
-
-    @Override
-    public void onShowPress(MotionEvent e) {}
-
-    @Override
-    public void onTouchEvent(RecyclerView recyclerView, MotionEvent motionEvent) {}
-
-    @Override
-    public void onRequestDisallowInterceptTouchEvent(boolean b) {}
-}
--- a/mobile/android/base/home/SearchEngineBar.java
+++ b/mobile/android/base/home/SearchEngineBar.java
@@ -12,21 +12,22 @@ import android.support.v7.widget.LinearL
 import android.support.v7.widget.RecyclerView;
 import android.util.AttributeSet;
 import android.util.DisplayMetrics;
 import android.util.TypedValue;
 import android.view.View;
 
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.mozglue.RobocopTarget;
+import org.mozilla.gecko.widget.RecyclerViewClickSupport;
 
 import java.util.List;
 
 public class SearchEngineBar extends RecyclerView
-        implements RecyclerViewItemClickListener.OnClickListener {
+        implements RecyclerViewClickSupport.OnItemClickListener {
     private static final String LOGTAG = SearchEngineBar.class.getSimpleName();
 
     private static final float ICON_CONTAINER_MIN_WIDTH_DP = 72;
     private static final float LABEL_CONTAINER_WIDTH_DP = 48;
     private static final float DIVIDER_HEIGHT_DP = 1;
 
     public interface OnSearchBarClickListener {
         void onSearchBarClickListener(SearchEngine searchEngine);
@@ -61,17 +62,19 @@ public class SearchEngineBar extends Rec
 
         mAdapter = new SearchEngineAdapter(context);
         mAdapter.setIconContainerWidth(mIconContainerWidth);
         mLayoutManager = new LinearLayoutManager(context);
         mLayoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
 
         setAdapter(mAdapter);
         setLayoutManager(mLayoutManager);
-        addOnItemTouchListener(new RecyclerViewItemClickListener(context, this, this));
+
+        RecyclerViewClickSupport.addTo(this)
+            .setOnItemClickListener(this);
     }
 
     public void setSearchEngines(List<SearchEngine> searchEngines) {
         mAdapter.setSearchEngines(searchEngines);
     }
 
     public void setOnSearchBarClickListener(OnSearchBarClickListener listener) {
         mOnSearchBarClickListener = listener;
@@ -109,36 +112,31 @@ public class SearchEngineBar extends Rec
     @Override
     public void onDraw(Canvas canvas) {
         super.onDraw(canvas);
 
         canvas.drawRect(0, 0, getWidth(), mDividerHeight, mDividerPaint);
     }
 
     @Override
-    public void onClick(View view, int position) {
+    public void onItemClicked(RecyclerView recyclerView, int position, View v) {
         if (mOnSearchBarClickListener == null) {
             throw new IllegalStateException(
                     OnSearchBarClickListener.class.getSimpleName() + " is not initializer."
             );
         }
 
         if (position == 0) {
             return;
         }
 
         final SearchEngine searchEngine = mAdapter.getItem(position);
         mOnSearchBarClickListener.onSearchBarClickListener(searchEngine);
     }
 
-    @Override
-    public void onLongClick(View view, int position) {
-        // do nothing
-    }
-
     /**
      * We manually add the override for getAdapter because we see this method getting stripped
      * out during compile time by aggressive proguard rules.
      */
     @RobocopTarget
     @Override
     public SearchEngineAdapter getAdapter() {
         return mAdapter;
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -337,17 +337,16 @@ gbjar.sources += [
     'home/PanelRecyclerViewAdapter.java',
     'home/PanelRefreshLayout.java',
     'home/PanelViewAdapter.java',
     'home/PanelViewItemHandler.java',
     'home/PinSiteDialog.java',
     'home/ReadingListPanel.java',
     'home/ReadingListRow.java',
     'home/RecentTabsPanel.java',
-    'home/RecyclerViewItemClickListener.java',
     'home/RemoteTabsBaseFragment.java',
     'home/RemoteTabsExpandableListFragment.java',
     'home/RemoteTabsExpandableListState.java',
     'home/RemoteTabsPanel.java',
     'home/RemoteTabsSplitPlaneFragment.java',
     'home/RemoteTabsStaticFragment.java',
     'home/SearchEngine.java',
     'home/SearchEngineAdapter.java',
@@ -534,16 +533,17 @@ gbjar.sources += [
     'widget/FloatingHintEditText.java',
     'widget/FlowLayout.java',
     'widget/GeckoActionProvider.java',
     'widget/GeckoPopupMenu.java',
     'widget/GeckoSwipeRefreshLayout.java',
     'widget/GeckoViewFlipper.java',
     'widget/IconTabWidget.java',
     'widget/LoginDoorHanger.java',
+    'widget/RecyclerViewClickSupport.java',
     'widget/ResizablePathDrawable.java',
     'widget/RoundedCornerLayout.java',
     'widget/SiteLogins.java',
     'widget/SquaredImageView.java',
     'widget/SquaredRelativeLayout.java',
     'widget/SwipeDismissListViewTouchListener.java',
     'widget/TabThumbnailWrapper.java',
     'widget/ThumbnailView.java',
--- a/mobile/android/base/resources/values/ids.xml
+++ b/mobile/android/base/resources/values/ids.xml
@@ -4,10 +4,11 @@
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
 <resources>
 
     <item type="id" name="tabQueueNotification"/>
     <item type="id" name="guestNotification"/>
     <item type="id" name="original_height"/>
     <item type="id" name="menu_items"/>
+    <item type="id" name="recycler_view_click_support" />
 
 </resources>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/widget/RecyclerViewClickSupport.java
@@ -0,0 +1,105 @@
+/* -*- 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.widget;
+
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+
+import org.mozilla.gecko.R;
+
+/**
+ * {@link RecyclerViewClickSupport} implementation that will notify an OnClickListener about clicks and long clicks
+ * on items displayed by the RecyclerView.
+ * @see <a href="http://www.littlerobots.nl/blog/Handle-Android-RecyclerView-Clicks/">littlerobots.nl</a>
+ */
+public class RecyclerViewClickSupport {
+    private final RecyclerView mRecyclerView;
+    private OnItemClickListener mOnItemClickListener;
+    private OnItemLongClickListener mOnItemLongClickListener;
+    private View.OnClickListener mOnClickListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            if (mOnItemClickListener != null) {
+                RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(v);
+                mOnItemClickListener.onItemClicked(mRecyclerView, holder.getAdapterPosition(), v);
+            }
+        }
+    };
+    private View.OnLongClickListener mOnLongClickListener = new View.OnLongClickListener() {
+        @Override
+        public boolean onLongClick(View v) {
+            if (mOnItemLongClickListener != null) {
+                RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(v);
+                return mOnItemLongClickListener.onItemLongClicked(mRecyclerView, holder.getAdapterPosition(), v);
+            }
+            return false;
+        }
+    };
+    private RecyclerView.OnChildAttachStateChangeListener mAttachListener
+            = new RecyclerView.OnChildAttachStateChangeListener() {
+        @Override
+        public void onChildViewAttachedToWindow(View view) {
+            if (mOnItemClickListener != null) {
+                view.setOnClickListener(mOnClickListener);
+            }
+            if (mOnItemLongClickListener != null) {
+                view.setOnLongClickListener(mOnLongClickListener);
+            }
+        }
+
+        @Override
+        public void onChildViewDetachedFromWindow(View view) {
+
+        }
+    };
+
+    private RecyclerViewClickSupport(RecyclerView recyclerView) {
+        mRecyclerView = recyclerView;
+        mRecyclerView.setTag(R.id.recycler_view_click_support, this);
+        mRecyclerView.addOnChildAttachStateChangeListener(mAttachListener);
+    }
+
+    public static RecyclerViewClickSupport addTo(RecyclerView view) {
+        RecyclerViewClickSupport support = (RecyclerViewClickSupport) view.getTag(R.id.recycler_view_click_support);
+        if (support == null) {
+            support = new RecyclerViewClickSupport(view);
+        }
+        return support;
+    }
+
+    public static RecyclerViewClickSupport removeFrom(RecyclerView view) {
+        RecyclerViewClickSupport support = (RecyclerViewClickSupport) view.getTag(R.id.recycler_view_click_support);
+        if (support != null) {
+            support.detach(view);
+        }
+        return support;
+    }
+
+    public RecyclerViewClickSupport setOnItemClickListener(OnItemClickListener listener) {
+        mOnItemClickListener = listener;
+        return this;
+    }
+
+    public RecyclerViewClickSupport setOnItemLongClickListener(OnItemLongClickListener listener) {
+        mOnItemLongClickListener = listener;
+        return this;
+    }
+
+    private void detach(RecyclerView view) {
+        view.removeOnChildAttachStateChangeListener(mAttachListener);
+        view.setTag(R.id.recycler_view_click_support, null);
+    }
+
+    public interface OnItemClickListener {
+
+        void onItemClicked(RecyclerView recyclerView, int position, View v);
+    }
+
+    public interface OnItemLongClickListener {
+
+        boolean onItemLongClicked(RecyclerView recyclerView, int position, View v);
+    }
+}