Adding methods in RecyclerView to allow overriding edgeeffect behavior
Bug: 38174616
Test: Added CustomEdgeEffectTest
Change-Id: I704be29256eee55a0511f323477085a6a2e37f9f
diff --git a/v7/recyclerview/api/current.txt b/v7/recyclerview/api/current.txt
index d484819..aa79213 100644
--- a/v7/recyclerview/api/current.txt
+++ b/v7/recyclerview/api/current.txt
@@ -312,6 +312,7 @@
method public android.support.v7.widget.RecyclerView.ViewHolder getChildViewHolder(android.view.View);
method public android.support.v7.widget.RecyclerViewAccessibilityDelegate getCompatAccessibilityDelegate();
method public void getDecoratedBoundsWithMargins(android.view.View, android.graphics.Rect);
+ method public android.support.v7.widget.RecyclerView.EdgeEffectFactory getEdgeEffectFactory();
method public android.support.v7.widget.RecyclerView.ItemAnimator getItemAnimator();
method public android.support.v7.widget.RecyclerView.ItemDecoration getItemDecorationAt(int);
method public int getItemDecorationCount();
@@ -346,6 +347,7 @@
method public void setAccessibilityDelegateCompat(android.support.v7.widget.RecyclerViewAccessibilityDelegate);
method public void setAdapter(android.support.v7.widget.RecyclerView.Adapter);
method public void setChildDrawingOrderCallback(android.support.v7.widget.RecyclerView.ChildDrawingOrderCallback);
+ method public void setEdgeEffectFactory(android.support.v7.widget.RecyclerView.EdgeEffectFactory);
method public void setHasFixedSize(boolean);
method public void setItemAnimator(android.support.v7.widget.RecyclerView.ItemAnimator);
method public void setItemViewCacheSize(int);
@@ -424,6 +426,18 @@
method public abstract int onGetChildDrawingOrder(int, int);
}
+ public static class RecyclerView.EdgeEffectFactory {
+ ctor public RecyclerView.EdgeEffectFactory();
+ method protected android.widget.EdgeEffect createEdgeEffect(android.support.v7.widget.RecyclerView, int);
+ field public static final int DIRECTION_BOTTOM = 3; // 0x3
+ field public static final int DIRECTION_LEFT = 0; // 0x0
+ field public static final int DIRECTION_RIGHT = 2; // 0x2
+ field public static final int DIRECTION_TOP = 1; // 0x1
+ }
+
+ public static abstract class RecyclerView.EdgeEffectFactory.EdgeDirection implements java.lang.annotation.Annotation {
+ }
+
public static abstract class RecyclerView.ItemAnimator {
ctor public RecyclerView.ItemAnimator();
method public abstract boolean animateAppearance(android.support.v7.widget.RecyclerView.ViewHolder, android.support.v7.widget.RecyclerView.ItemAnimator.ItemHolderInfo, android.support.v7.widget.RecyclerView.ItemAnimator.ItemHolderInfo);
diff --git a/v7/recyclerview/src/main/java/android/support/v7/widget/RecyclerView.java b/v7/recyclerview/src/main/java/android/support/v7/widget/RecyclerView.java
index dea8546..7009733 100644
--- a/v7/recyclerview/src/main/java/android/support/v7/widget/RecyclerView.java
+++ b/v7/recyclerview/src/main/java/android/support/v7/widget/RecyclerView.java
@@ -44,6 +44,7 @@
import android.support.annotation.RestrictTo;
import android.support.annotation.VisibleForTesting;
import android.support.v4.os.TraceCompat;
+import android.support.v4.util.Preconditions;
import android.support.v4.view.AbsSavedState;
import android.support.v4.view.InputDeviceCompat;
import android.support.v4.view.MotionEventCompat;
@@ -417,6 +418,8 @@
*/
private int mDispatchScrollCounter = 0;
+ @NonNull
+ private EdgeEffectFactory mEdgeEffectFactory = new EdgeEffectFactory();
private EdgeEffect mLeftGlow, mTopGlow, mRightGlow, mBottomGlow;
ItemAnimator mItemAnimator = new DefaultItemAnimator();
@@ -2306,7 +2309,7 @@
if (mLeftGlow != null) {
return;
}
- mLeftGlow = new EdgeEffect(getContext());
+ mLeftGlow = mEdgeEffectFactory.createEdgeEffect(this, EdgeEffectFactory.DIRECTION_LEFT);
if (mClipToPadding) {
mLeftGlow.setSize(getMeasuredHeight() - getPaddingTop() - getPaddingBottom(),
getMeasuredWidth() - getPaddingLeft() - getPaddingRight());
@@ -2319,7 +2322,7 @@
if (mRightGlow != null) {
return;
}
- mRightGlow = new EdgeEffect(getContext());
+ mRightGlow = mEdgeEffectFactory.createEdgeEffect(this, EdgeEffectFactory.DIRECTION_RIGHT);
if (mClipToPadding) {
mRightGlow.setSize(getMeasuredHeight() - getPaddingTop() - getPaddingBottom(),
getMeasuredWidth() - getPaddingLeft() - getPaddingRight());
@@ -2332,7 +2335,7 @@
if (mTopGlow != null) {
return;
}
- mTopGlow = new EdgeEffect(getContext());
+ mTopGlow = mEdgeEffectFactory.createEdgeEffect(this, EdgeEffectFactory.DIRECTION_TOP);
if (mClipToPadding) {
mTopGlow.setSize(getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
getMeasuredHeight() - getPaddingTop() - getPaddingBottom());
@@ -2346,7 +2349,7 @@
if (mBottomGlow != null) {
return;
}
- mBottomGlow = new EdgeEffect(getContext());
+ mBottomGlow = mEdgeEffectFactory.createEdgeEffect(this, EdgeEffectFactory.DIRECTION_BOTTOM);
if (mClipToPadding) {
mBottomGlow.setSize(getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
getMeasuredHeight() - getPaddingTop() - getPaddingBottom());
@@ -2360,6 +2363,32 @@
}
/**
+ * Set a {@link EdgeEffectFactory} for this {@link RecyclerView}.
+ * <p>
+ * When a new {@link EdgeEffectFactory} is set, any existing over-scroll effects are cleared
+ * and new effects are created as needed using
+ * {@link EdgeEffectFactory#createEdgeEffect(RecyclerView, int)}
+ *
+ * @param edgeEffectFactory The {@link EdgeEffectFactory} instance.
+ */
+ public void setEdgeEffectFactory(@NonNull EdgeEffectFactory edgeEffectFactory) {
+ Preconditions.checkNotNull(edgeEffectFactory);
+ mEdgeEffectFactory = edgeEffectFactory;
+ invalidateGlows();
+ }
+
+ /**
+ * Retrieves the previously set {@link EdgeEffectFactory} or the default factory if nothing
+ * was set.
+ *
+ * @return The previously set {@link EdgeEffectFactory}
+ * @see #setEdgeEffectFactory(EdgeEffectFactory)
+ */
+ public EdgeEffectFactory getEdgeEffectFactory() {
+ return mEdgeEffectFactory;
+ }
+
+ /**
* Since RecyclerView is a collection ViewGroup that includes virtual children (items that are
* in the Adapter but not visible in the UI), it employs a more involved focus search strategy
* that differs from other ViewGroups.
@@ -5130,6 +5159,46 @@
}
/**
+ * EdgeEffectFactory lets you customize the over-scroll edge effect for RecyclerViews.
+ *
+ * @see RecyclerView#setEdgeEffectFactory(EdgeEffectFactory)
+ */
+ public static class EdgeEffectFactory {
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({DIRECTION_LEFT, DIRECTION_TOP, DIRECTION_RIGHT, DIRECTION_BOTTOM})
+ public @interface EdgeDirection {}
+
+ /**
+ * Direction constant for the left edge
+ */
+ public static final int DIRECTION_LEFT = 0;
+
+ /**
+ * Direction constant for the top edge
+ */
+ public static final int DIRECTION_TOP = 1;
+
+ /**
+ * Direction constant for the right edge
+ */
+ public static final int DIRECTION_RIGHT = 2;
+
+ /**
+ * Direction constant for the bottom edge
+ */
+ public static final int DIRECTION_BOTTOM = 3;
+
+ /**
+ * Create a new EdgeEffect for the provided direction.
+ */
+ protected @NonNull EdgeEffect createEdgeEffect(RecyclerView view,
+ @EdgeDirection int direction) {
+ return new EdgeEffect(view.getContext());
+ }
+ }
+
+ /**
* RecycledViewPool lets you share Views between multiple RecyclerViews.
* <p>
* If you want to recycle views across RecyclerViews, create an instance of RecycledViewPool
diff --git a/v7/recyclerview/tests/src/android/support/v7/util/TouchUtils.java b/v7/recyclerview/tests/src/android/support/v7/util/TouchUtils.java
index 1a64e3c..418d33f 100644
--- a/v7/recyclerview/tests/src/android/support/v7/util/TouchUtils.java
+++ b/v7/recyclerview/tests/src/android/support/v7/util/TouchUtils.java
@@ -154,6 +154,18 @@
inst.waitForIdleSync();
}
+ public static void scrollView(int axis, int axisValue, int inputDevice, View v) {
+ MotionEvent.PointerProperties[] pointerProperties = { new MotionEvent.PointerProperties() };
+ MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
+ coords.setAxisValue(axis, axisValue);
+ MotionEvent.PointerCoords[] pointerCoords = { coords };
+ MotionEvent e = MotionEvent.obtain(
+ 0, System.currentTimeMillis(), MotionEvent.ACTION_SCROLL,
+ 1, pointerProperties, pointerCoords, 0, 0, 1, 1, 0, 0, inputDevice, 0);
+ v.onGenericMotionEvent(e);
+ e.recycle();
+ }
+
public static void dragViewToTop(Instrumentation inst, View v) {
dragViewToTop(inst, v, calculateStepsForDistance(v.getTop()));
}
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/CustomEdgeEffectTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/CustomEdgeEffectTest.java
new file mode 100644
index 0000000..b587d51
--- /dev/null
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/CustomEdgeEffectTest.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v7.widget;
+
+
+import static android.support.v7.widget.RecyclerView.EdgeEffectFactory;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v4.view.InputDeviceCompat;
+import android.support.v7.util.TouchUtils;
+import android.view.MotionEvent;
+import android.view.ViewGroup;
+import android.widget.EdgeEffect;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests custom edge effect are properly applied when scrolling.
+ */
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class CustomEdgeEffectTest extends BaseRecyclerViewInstrumentationTest {
+
+ private static final int NUM_ITEMS = 10;
+
+ private LinearLayoutManager mLayoutManager;
+ private RecyclerView mRecyclerView;
+
+ @Before
+ public void setup() throws Throwable {
+ mLayoutManager = new LinearLayoutManager(getActivity());
+ mLayoutManager.ensureLayoutState();
+
+ mRecyclerView = new RecyclerView(getActivity());
+ mRecyclerView.setLayoutManager(mLayoutManager);
+ mRecyclerView.setAdapter(new TestAdapter(NUM_ITEMS) {
+
+ @Override
+ public TestViewHolder onCreateViewHolder(ViewGroup parent,
+ int viewType) {
+ TestViewHolder holder = super.onCreateViewHolder(parent, viewType);
+ holder.itemView.setMinimumHeight(mRecyclerView.getMeasuredHeight() * 2 / NUM_ITEMS);
+ return holder;
+ }
+ });
+ setRecyclerView(mRecyclerView);
+ getInstrumentation().waitForIdleSync();
+ assertThat("Test sanity", mRecyclerView.getChildCount() > 0, is(true));
+ }
+
+ @Test
+ public void testEdgeEffectDirections() throws Throwable {
+ TestEdgeEffectFactory factory = new TestEdgeEffectFactory();
+ mRecyclerView.setEdgeEffectFactory(factory);
+ scrollToPosition(0);
+ waitForIdleScroll(mRecyclerView);
+ scrollViewBy(3);
+ assertNull(factory.mBottom);
+ assertNotNull(factory.mTop);
+ assertTrue(factory.mTop.mPullDistance > 0);
+
+ scrollToPosition(NUM_ITEMS - 1);
+ waitForIdleScroll(mRecyclerView);
+ scrollViewBy(-3);
+
+ assertNotNull(factory.mBottom);
+ assertTrue(factory.mBottom.mPullDistance > 0);
+ }
+
+ @Test
+ public void testEdgeEffectReplaced() throws Throwable {
+ TestEdgeEffectFactory factory1 = new TestEdgeEffectFactory();
+ mRecyclerView.setEdgeEffectFactory(factory1);
+ scrollToPosition(0);
+ waitForIdleScroll(mRecyclerView);
+
+ scrollViewBy(3);
+ assertNotNull(factory1.mTop);
+ float oldPullDistance = factory1.mTop.mPullDistance;
+
+ waitForIdleScroll(mRecyclerView);
+ TestEdgeEffectFactory factory2 = new TestEdgeEffectFactory();
+ mRecyclerView.setEdgeEffectFactory(factory2);
+ scrollViewBy(30);
+ assertNotNull(factory2.mTop);
+
+ assertTrue(factory2.mTop.mPullDistance > oldPullDistance);
+ assertEquals(oldPullDistance, factory1.mTop.mPullDistance, 0.1f);
+ }
+
+ private void scrollViewBy(final int value) throws Throwable {
+ mActivityRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ TouchUtils.scrollView(MotionEvent.AXIS_VSCROLL, value,
+ InputDeviceCompat.SOURCE_CLASS_POINTER, mRecyclerView);
+ }
+ });
+ }
+
+ private class TestEdgeEffectFactory extends EdgeEffectFactory {
+
+ TestEdgeEffect mTop, mBottom;
+
+ @NonNull
+ @Override
+ protected EdgeEffect createEdgeEffect(RecyclerView view, int direction) {
+ TestEdgeEffect effect = new TestEdgeEffect(view.getContext());
+ if (direction == EdgeEffectFactory.DIRECTION_TOP) {
+ mTop = effect;
+ } else if (direction == EdgeEffectFactory.DIRECTION_BOTTOM) {
+ mBottom = effect;
+ }
+ return effect;
+ }
+ }
+
+ private class TestEdgeEffect extends EdgeEffect {
+
+ private float mPullDistance;
+
+ TestEdgeEffect(Context context) {
+ super(context);
+ }
+
+ @Override
+ public void onPull(float deltaDistance, float displacement) {
+ mPullDistance = deltaDistance;
+ super.onPull(deltaDistance, displacement);
+ }
+ }
+}
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewOnGenericMotionEventTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewOnGenericMotionEventTest.java
index aee15dd..2f80156 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewOnGenericMotionEventTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewOnGenericMotionEventTest.java
@@ -24,8 +24,8 @@
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
import android.support.v4.view.InputDeviceCompat;
-import android.support.v4.view.MotionEventCompat;
import android.support.v4.view.ViewConfigurationCompat;
+import android.support.v7.util.TouchUtils;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
@@ -60,9 +60,8 @@
MockLayoutManager layoutManager = new MockLayoutManager(true, true);
mRecyclerView.setLayoutManager(layoutManager);
layout();
- MotionEvent e = obtainScrollMotionEvent(
- MotionEventCompat.AXIS_SCROLL, 2, InputDeviceCompat.SOURCE_ROTARY_ENCODER);
- mRecyclerView.onGenericMotionEvent(e);
+ TouchUtils.scrollView(
+ MotionEvent.AXIS_SCROLL, 2, InputDeviceCompat.SOURCE_ROTARY_ENCODER, mRecyclerView);
assertTotalScroll(0, (int) (-2f * getScaledVerticalScrollFactor()));
}
@@ -73,9 +72,8 @@
MockLayoutManager layoutManager = new MockLayoutManager(true, false);
mRecyclerView.setLayoutManager(layoutManager);
layout();
- MotionEvent e = obtainScrollMotionEvent(
- MotionEventCompat.AXIS_SCROLL, 2, InputDeviceCompat.SOURCE_ROTARY_ENCODER);
- mRecyclerView.onGenericMotionEvent(e);
+ TouchUtils.scrollView(
+ MotionEvent.AXIS_SCROLL, 2, InputDeviceCompat.SOURCE_ROTARY_ENCODER, mRecyclerView);
assertTotalScroll((int) (2f * getScaledHorizontalScrollFactor()), 0);
}
@@ -84,9 +82,8 @@
MockLayoutManager layoutManager = new MockLayoutManager(true, true);
mRecyclerView.setLayoutManager(layoutManager);
layout();
- MotionEvent e = obtainScrollMotionEvent(
- MotionEventCompat.AXIS_VSCROLL, 2, InputDeviceCompat.SOURCE_CLASS_POINTER);
- mRecyclerView.onGenericMotionEvent(e);
+ TouchUtils.scrollView(
+ MotionEvent.AXIS_VSCROLL, 2, InputDeviceCompat.SOURCE_CLASS_POINTER, mRecyclerView);
assertTotalScroll(0, (int) (-2f * getScaledVerticalScrollFactor()));
}
@@ -95,9 +92,8 @@
MockLayoutManager layoutManager = new MockLayoutManager(true, true);
mRecyclerView.setLayoutManager(layoutManager);
layout();
- MotionEvent e = obtainScrollMotionEvent(
- MotionEventCompat.AXIS_HSCROLL, 2, InputDeviceCompat.SOURCE_CLASS_POINTER);
- mRecyclerView.onGenericMotionEvent(e);
+ TouchUtils.scrollView(
+ MotionEvent.AXIS_HSCROLL, 2, InputDeviceCompat.SOURCE_CLASS_POINTER, mRecyclerView);
assertTotalScroll((int) (2f * getScaledHorizontalScrollFactor()), 0);
}