Add PagerSnapHelper to enable ViewPager-like behavior.
- Adds PagerSnapHelper.
- Adds a simple PagerRecyclerView example
- Adds snap toggle to LinearLayoutManagerActivity.
Test: Added PagerSnapHelperTest
Change-Id: Ia0f3b59b9c40fdbc4646d89feb5337d43d570916
diff --git a/api/current.txt b/api/current.txt
index 7b2416c5..1597af1 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -9857,6 +9857,13 @@
field protected final android.support.v7.widget.RecyclerView.LayoutManager mLayoutManager;
}
+ public class PagerSnapHelper extends android.support.v7.widget.SnapHelper {
+ ctor public PagerSnapHelper();
+ method public int[] calculateDistanceToFinalSnap(android.support.v7.widget.RecyclerView.LayoutManager, android.view.View);
+ method public android.view.View findSnapView(android.support.v7.widget.RecyclerView.LayoutManager);
+ method public int findTargetSnapPosition(android.support.v7.widget.RecyclerView.LayoutManager, int, int);
+ }
+
public class PopupMenu {
ctor public PopupMenu(android.content.Context, android.view.View);
ctor public PopupMenu(android.content.Context, android.view.View, int);
diff --git a/samples/Support7Demos/AndroidManifest.xml b/samples/Support7Demos/AndroidManifest.xml
index aac5a6d..0e9005a 100644
--- a/samples/Support7Demos/AndroidManifest.xml
+++ b/samples/Support7Demos/AndroidManifest.xml
@@ -370,6 +370,15 @@
</intent-filter>
</activity>
+ <activity android:name=".widget.PagerRecyclerViewActivity"
+ android:label="@string/pager_recycler_view"
+ android:theme="@style/Theme.AppCompat">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="com.example.android.supportv7.SAMPLE_CODE" />
+ </intent-filter>
+ </activity>
+
<activity android:name=".widget.AnimatedRecyclerView"
android:label="@string/animated_recycler_view"
android:theme="@style/Theme.AppCompat">
diff --git a/samples/Support7Demos/res/values/strings.xml b/samples/Support7Demos/res/values/strings.xml
index afb37b0..10f6761 100644
--- a/samples/Support7Demos/res/values/strings.xml
+++ b/samples/Support7Demos/res/values/strings.xml
@@ -132,6 +132,7 @@
<string name="sample_media_route_activity_presentation">Local Playback on Presentation Display</string>
<string name="recycler_view">RecyclerView/RecyclerViewActivity</string>
+ <string name="pager_recycler_view">RecyclerView/PagerRecyclerViewActivity</string>
<string name="animated_recycler_view">RecyclerView/Animated RecyclerView</string>
<string name="nested_recycler_view">RecyclerView/Nested RecyclerView</string>
<string name="linear_layout_manager">RecyclerView/Linear Layout Manager</string>
@@ -143,6 +144,7 @@
<string name="checkbox_reverse">Rev.</string>
<string name="checkbox_layout_dir">Layout Dir</string>
<string name="checkbox_stack_from_end">Stack From End</string>
+ <string name="checkbox_snap">Snap</string>
<string name="enableAnimations">Animate</string>
<string name="enablePredictiveAnimations">Predictive</string>
<string name="enableInPlaceChange">In Place Change</string>
diff --git a/samples/Support7Demos/src/com/example/android/supportv7/widget/LinearLayoutManagerActivity.java b/samples/Support7Demos/src/com/example/android/supportv7/widget/LinearLayoutManagerActivity.java
index 31d53cc..184beaa 100644
--- a/samples/Support7Demos/src/com/example/android/supportv7/widget/LinearLayoutManagerActivity.java
+++ b/samples/Support7Demos/src/com/example/android/supportv7/widget/LinearLayoutManagerActivity.java
@@ -19,6 +19,7 @@
import android.support.v4.view.ViewCompat;
import android.support.v7.widget.DividerItemDecoration;
import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.LinearSnapHelper;
import android.support.v7.widget.RecyclerView;
import com.example.android.supportv7.R;
@@ -29,6 +30,8 @@
*/
public class LinearLayoutManagerActivity extends BaseLayoutManagerActivity<LinearLayoutManager> {
private DividerItemDecoration mDividerItemDecoration;
+ private LinearSnapHelper mLinearSnapHelper;
+ private boolean mSnapHelperAttached;
@Override
protected LinearLayoutManager createLayoutManager() {
@@ -39,6 +42,7 @@
protected void onRecyclerViewInit(RecyclerView recyclerView) {
mDividerItemDecoration = new DividerItemDecoration(this, mLayoutManager.getOrientation());
recyclerView.addItemDecoration(mDividerItemDecoration);
+ mLinearSnapHelper = new LinearSnapHelper();
}
@Override
@@ -94,6 +98,18 @@
public void onChange(boolean newValue) {
mLayoutManager.setStackFromEnd(newValue);
}
+ },
+ new ConfigToggle(this, R.string.checkbox_snap) {
+ @Override
+ public boolean isChecked() {
+ return mSnapHelperAttached;
+ }
+
+ @Override
+ public void onChange(boolean newValue) {
+ mLinearSnapHelper.attachToRecyclerView(newValue ? mRecyclerView : null);
+ mSnapHelperAttached = newValue;
+ }
}
};
}
diff --git a/samples/Support7Demos/src/com/example/android/supportv7/widget/PagerRecyclerViewActivity.java b/samples/Support7Demos/src/com/example/android/supportv7/widget/PagerRecyclerViewActivity.java
new file mode 100644
index 0000000..bb0c248
--- /dev/null
+++ b/samples/Support7Demos/src/com/example/android/supportv7/widget/PagerRecyclerViewActivity.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2016 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 com.example.android.supportv7.widget;
+
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.PagerSnapHelper;
+import android.support.v7.widget.RecyclerView;
+import android.view.ViewGroup;
+
+import com.example.android.supportv7.Cheeses;
+import com.example.android.supportv7.widget.adapter.SimpleStringAdapter;
+
+/**
+ * Example activity that uses LinearLayoutManager, RecyclerView, and PagerSnapHelper.
+ */
+public class PagerRecyclerViewActivity extends AppCompatActivity {
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ final RecyclerView rv = new RecyclerView(this);
+ final LinearLayoutManager manager =
+ new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
+ rv.setLayoutManager(manager);
+ rv.setHasFixedSize(true);
+ rv.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT));
+ rv.setAdapter(new SimpleStringAdapter(this, Cheeses.sCheeseStrings) {
+ @Override
+ public RecyclerView.LayoutParams getLayoutParams() {
+ return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT);
+ }
+ });
+ PagerSnapHelper snapHelper = new PagerSnapHelper();
+ snapHelper.attachToRecyclerView(rv);
+ setContentView(rv);
+ }
+}
diff --git a/samples/Support7Demos/src/com/example/android/supportv7/widget/adapter/SimpleStringAdapter.java b/samples/Support7Demos/src/com/example/android/supportv7/widget/adapter/SimpleStringAdapter.java
index 04161ea..f9c4ae4 100644
--- a/samples/Support7Demos/src/com/example/android/supportv7/widget/adapter/SimpleStringAdapter.java
+++ b/samples/Support7Demos/src/com/example/android/supportv7/widget/adapter/SimpleStringAdapter.java
@@ -27,6 +27,9 @@
import java.util.Collections;
import java.util.List;
+/**
+ * A simple RecyclerView adapter that displays every string passed in a constructor as an item.
+ */
public class SimpleStringAdapter extends RecyclerView.Adapter<SimpleStringAdapter.ViewHolder> {
private int mBackground;
@@ -78,13 +81,7 @@
h.mTextView.setPadding(20, 0, 20, 0);
h.mTextView.setFocusable(true);
h.mTextView.setBackgroundResource(mBackground);
- RecyclerView.LayoutParams lp = new RecyclerView.LayoutParams(
- ViewGroup.LayoutParams.WRAP_CONTENT,
- ViewGroup.LayoutParams.WRAP_CONTENT);
- lp.leftMargin = 10;
- lp.rightMargin = 5;
- lp.topMargin = 20;
- lp.bottomMargin = 15;
+ RecyclerView.LayoutParams lp = getLayoutParams();
h.mTextView.setLayoutParams(lp);
return h;
}
@@ -97,6 +94,23 @@
holder.mTextView.setBackgroundColor(getBackgroundColor(position));
}
+
+ /**
+ * Returns LayoutParams to be used for each item in this adapter. It can be overridden
+ * to provide different LayoutParams.
+ * @return LayoutParams to be used for each item in this adapter.
+ */
+ public RecyclerView.LayoutParams getLayoutParams() {
+ RecyclerView.LayoutParams lp = new RecyclerView.LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT);
+ lp.leftMargin = 10;
+ lp.rightMargin = 5;
+ lp.topMargin = 20;
+ lp.bottomMargin = 15;
+ return lp;
+ }
+
private int getBackgroundColor(int position) {
switch (position % 4) {
case 0: return Color.BLACK;
diff --git a/v7/recyclerview/src/android/support/v7/widget/PagerSnapHelper.java b/v7/recyclerview/src/android/support/v7/widget/PagerSnapHelper.java
new file mode 100644
index 0000000..169a1b1
--- /dev/null
+++ b/v7/recyclerview/src/android/support/v7/widget/PagerSnapHelper.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright (C) 2016 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 android.graphics.PointF;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.view.View;
+
+/**
+ * Implementation of the {@link SnapHelper} supporting pager style snapping in either vertical or
+ * horizontal orientation.
+ */
+public class PagerSnapHelper extends SnapHelper {
+ // Orientation helpers are lazily created per LayoutManager.
+ @Nullable
+ private OrientationHelper mVerticalHelper;
+ @Nullable
+ private OrientationHelper mHorizontalHelper;
+
+ @Nullable
+ @Override
+ public int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager,
+ @NonNull View targetView) {
+ int[] out = new int[2];
+ if (layoutManager.canScrollHorizontally()) {
+ out[0] = distanceToCenter(layoutManager, targetView,
+ getHorizontalHelper(layoutManager));
+ } else {
+ out[0] = 0;
+ }
+
+ if (layoutManager.canScrollVertically()) {
+ out[1] = distanceToCenter(layoutManager, targetView,
+ getVerticalHelper(layoutManager));
+ } else {
+ out[1] = 0;
+ }
+ return out;
+ }
+
+ @Nullable
+ @Override
+ public View findSnapView(RecyclerView.LayoutManager layoutManager) {
+ if (layoutManager.canScrollVertically()) {
+ return findCenterView(layoutManager, getVerticalHelper(layoutManager));
+ } else if (layoutManager.canScrollHorizontally()) {
+ return findCenterView(layoutManager, getHorizontalHelper(layoutManager));
+ }
+ return null;
+ }
+
+ @Override
+ public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,
+ int velocityY) {
+ final int itemCount = layoutManager.getItemCount();
+ if (itemCount == 0) {
+ return RecyclerView.NO_POSITION;
+ }
+
+ View mStartMostChildView = null;
+ if (layoutManager.canScrollVertically()) {
+ mStartMostChildView = findStartView(layoutManager, getVerticalHelper(layoutManager));
+ } else if (layoutManager.canScrollHorizontally()) {
+ mStartMostChildView = findStartView(layoutManager, getHorizontalHelper(layoutManager));
+ }
+
+ if (mStartMostChildView == null) {
+ return RecyclerView.NO_POSITION;
+ }
+ final int centerPosition = layoutManager.getPosition(mStartMostChildView);
+ if (centerPosition == RecyclerView.NO_POSITION) {
+ return RecyclerView.NO_POSITION;
+ }
+
+ final boolean forwardDirection;
+ if (layoutManager.canScrollHorizontally()) {
+ forwardDirection = velocityX > 0;
+ } else {
+ forwardDirection = velocityY > 0;
+ }
+ boolean reverseLayout = false;
+ if ((layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
+ RecyclerView.SmoothScroller.ScrollVectorProvider vectorProvider =
+ (RecyclerView.SmoothScroller.ScrollVectorProvider) layoutManager;
+ PointF vectorForEnd = vectorProvider.computeScrollVectorForPosition(itemCount - 1);
+ if (vectorForEnd != null) {
+ reverseLayout = vectorForEnd.x < 0 || vectorForEnd.y < 0;
+ }
+ }
+ return reverseLayout
+ ? (forwardDirection ? centerPosition - 1 : centerPosition)
+ : (forwardDirection ? centerPosition + 1 : centerPosition);
+ }
+
+ private int distanceToCenter(@NonNull RecyclerView.LayoutManager layoutManager,
+ @NonNull View targetView, OrientationHelper helper) {
+ final int childCenter = helper.getDecoratedStart(targetView)
+ + (helper.getDecoratedMeasurement(targetView) / 2);
+ final int containerCenter;
+ if (layoutManager.getClipToPadding()) {
+ containerCenter = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
+ } else {
+ containerCenter = helper.getEnd() / 2;
+ }
+ return childCenter - containerCenter;
+ }
+
+ /**
+ * Return the child view that is currently closest to the center of this parent.
+ *
+ * @param layoutManager The {@link RecyclerView.LayoutManager} associated with the attached
+ * {@link RecyclerView}.
+ * @param helper The relevant {@link OrientationHelper} for the attached {@link RecyclerView}.
+ *
+ * @return the child view that is currently closest to the center of this parent.
+ */
+ @Nullable
+ private View findCenterView(RecyclerView.LayoutManager layoutManager,
+ OrientationHelper helper) {
+ int childCount = layoutManager.getChildCount();
+ if (childCount == 0) {
+ return null;
+ }
+
+ View closestChild = null;
+ final int center;
+ if (layoutManager.getClipToPadding()) {
+ center = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
+ } else {
+ center = helper.getEnd() / 2;
+ }
+ int absClosest = Integer.MAX_VALUE;
+
+ for (int i = 0; i < childCount; i++) {
+ final View child = layoutManager.getChildAt(i);
+ int childCenter = helper.getDecoratedStart(child)
+ + (helper.getDecoratedMeasurement(child) / 2);
+ int absDistance = Math.abs(childCenter - center);
+
+ /** if child center is closer than previous closest, set it as closest **/
+ if (absDistance < absClosest) {
+ absClosest = absDistance;
+ closestChild = child;
+ }
+ }
+ return closestChild;
+ }
+
+ /**
+ * Return the child view that is currently closest to the start of this parent.
+ *
+ * @param layoutManager The {@link RecyclerView.LayoutManager} associated with the attached
+ * {@link RecyclerView}.
+ * @param helper The relevant {@link OrientationHelper} for the attached {@link RecyclerView}.
+ *
+ * @return the child view that is currently closest to the start of this parent.
+ */
+ @Nullable
+ private View findStartView(RecyclerView.LayoutManager layoutManager,
+ OrientationHelper helper) {
+ int childCount = layoutManager.getChildCount();
+ if (childCount == 0) {
+ return null;
+ }
+
+ View closestChild = null;
+ int startest = Integer.MAX_VALUE;
+
+ for (int i = 0; i < childCount; i++) {
+ final View child = layoutManager.getChildAt(i);
+ int childStart = helper.getDecoratedStart(child);
+
+ /** if child is more to start than previous closest, set it as closest **/
+ if (childStart < startest) {
+ startest = childStart;
+ closestChild = child;
+ }
+ }
+ return closestChild;
+ }
+
+ @NonNull
+ private OrientationHelper getVerticalHelper(@NonNull RecyclerView.LayoutManager layoutManager) {
+ if (mVerticalHelper == null || mVerticalHelper.mLayoutManager != layoutManager) {
+ mVerticalHelper = OrientationHelper.createVerticalHelper(layoutManager);
+ }
+ return mVerticalHelper;
+ }
+
+ @NonNull
+ private OrientationHelper getHorizontalHelper(
+ @NonNull RecyclerView.LayoutManager layoutManager) {
+ if (mHorizontalHelper == null || mHorizontalHelper.mLayoutManager != layoutManager) {
+ mHorizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager);
+ }
+ return mHorizontalHelper;
+ }
+}
diff --git a/v7/recyclerview/src/android/support/v7/widget/SnapHelper.java b/v7/recyclerview/src/android/support/v7/widget/SnapHelper.java
index 37197e4..0d15d91 100644
--- a/v7/recyclerview/src/android/support/v7/widget/SnapHelper.java
+++ b/v7/recyclerview/src/android/support/v7/widget/SnapHelper.java
@@ -19,11 +19,11 @@
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView.LayoutManager;
+import android.support.v7.widget.RecyclerView.SmoothScroller.ScrollVectorProvider;
import android.util.DisplayMetrics;
import android.view.View;
import android.view.animation.DecelerateInterpolator;
import android.widget.Scroller;
-import android.support.v7.widget.RecyclerView.SmoothScroller.ScrollVectorProvider;
/**
* Class intended to support snapping for a {@link RecyclerView}.
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/PagerSnapHelperTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/PagerSnapHelperTest.java
new file mode 100644
index 0000000..0799d3d7
--- /dev/null
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/PagerSnapHelperTest.java
@@ -0,0 +1,289 @@
+/*
+ * Copyright (C) 2016 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 junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertNotSame;
+import static junit.framework.Assert.assertSame;
+import static junit.framework.Assert.assertTrue;
+
+import android.support.annotation.Nullable;
+import android.support.test.filters.MediumTest;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+@RunWith(Parameterized.class)
+public class PagerSnapHelperTest extends BaseLinearLayoutManagerTest {
+
+ final Config mConfig;
+ final boolean mReverseScroll;
+
+ public PagerSnapHelperTest(Config config, boolean reverseScroll) {
+ mConfig = config;
+ mReverseScroll = reverseScroll;
+ }
+
+ @Parameterized.Parameters(name = "config:{0},reverseScroll:{1}")
+ public static List<Object[]> getParams() {
+ List<Object[]> result = new ArrayList<>();
+ List<Config> configs = createBaseVariations();
+ for (Config config : configs) {
+ for (boolean reverseScroll : new boolean[] {false, true}) {
+ result.add(new Object[]{config, reverseScroll});
+ }
+ }
+ return result;
+ }
+
+ @MediumTest
+ @Test
+ public void snapOnScrollSameView() throws Throwable {
+ final Config config = (Config) mConfig.clone();
+ setupByConfig(config, true,
+ new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT),
+ new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT));
+ setupSnapHelper();
+
+ // Record the current center view.
+ TextView view = (TextView) findCenterView(mLayoutManager);
+ assertCenterAligned(view);
+
+ int scrollDistance = (getViewDimension(view) / 2) - 1;
+ int scrollDist = mReverseScroll ? -scrollDistance : scrollDistance;
+ mLayoutManager.expectIdleState(3);
+ smoothScrollBy(scrollDist);
+ mLayoutManager.waitForSnap(10);
+
+ // Views have not changed
+ View viewAfterFling = findCenterView(mLayoutManager);
+ assertSame("The view should NOT have scrolled", view, viewAfterFling);
+ assertCenterAligned(viewAfterFling);
+ }
+
+ @MediumTest
+ @Test
+ public void snapOnScrollNextView() throws Throwable {
+ final Config config = (Config) mConfig.clone();
+ setupByConfig(config, true,
+ new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT),
+ new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT));
+ setupSnapHelper();
+
+ // Record the current center view.
+ View view = findCenterView(mLayoutManager);
+ assertCenterAligned(view);
+
+ int scrollDistance = (getViewDimension(view) / 2) + 1;
+ int scrollDist = mReverseScroll ? -scrollDistance : scrollDistance;
+ mLayoutManager.expectIdleState(3);
+ smoothScrollBy(scrollDist);
+ mLayoutManager.waitForSnap(10);
+
+ // Views have not changed
+ View viewAfterFling = findCenterView(mLayoutManager);
+ assertNotSame("The view should have scrolled", view, viewAfterFling);
+ int expectedPosition = mConfig.mItemCount / 2 + (mConfig.mReverseLayout
+ ? (mReverseScroll ? 1 : -1)
+ : (mReverseScroll ? -1 : 1));
+ assertEquals(expectedPosition, mLayoutManager.getPosition(viewAfterFling));
+ assertCenterAligned(viewAfterFling);
+ }
+
+ @MediumTest
+ @Test
+ public void snapOnFlingSameView() throws Throwable {
+ final Config config = (Config) mConfig.clone();
+ setupByConfig(config, true,
+ new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT),
+ new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT));
+ setupSnapHelper();
+
+ // Record the current center view.
+ View view = findCenterView(mLayoutManager);
+ assertCenterAligned(view);
+
+ // Velocity small enough to not scroll to the next view.
+ int velocity = (int) (1.000001 * mRecyclerView.getMinFlingVelocity());
+ int velocityDir = mReverseScroll ? -velocity : velocity;
+ mLayoutManager.expectIdleState(2);
+ // Scroll at one pixel in the correct direction to allow fling snapping to the next view.
+ mActivityRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mRecyclerView.scrollBy(mReverseScroll ? -1 : 1, mReverseScroll ? -1 : 1);
+ }
+ });
+ waitForIdleScroll(mRecyclerView);
+ assertTrue(fling(velocityDir, velocityDir));
+ // Wait for two settling scrolls: the initial one and the corrective one.
+ waitForIdleScroll(mRecyclerView);
+ mLayoutManager.waitForSnap(100);
+
+ View viewAfterFling = findCenterView(mLayoutManager);
+
+ assertSame("The view should NOT have scrolled", view, viewAfterFling);
+ assertCenterAligned(viewAfterFling);
+ }
+
+ @MediumTest
+ @Test
+ public void snapOnFlingNextView() throws Throwable {
+ final Config config = (Config) mConfig.clone();
+ setupByConfig(config, true,
+ new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT),
+ new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT));
+ setupSnapHelper();
+ runSnapOnMaxFlingNextView((int) (0.2 * mRecyclerView.getMaxFlingVelocity()));
+ }
+
+ @MediumTest
+ @Test
+ public void snapOnMaxFlingNextView() throws Throwable {
+ final Config config = (Config) mConfig.clone();
+ setupByConfig(config, true,
+ new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT),
+ new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT));
+ setupSnapHelper();
+ runSnapOnMaxFlingNextView(mRecyclerView.getMaxFlingVelocity());
+ }
+
+ private void runSnapOnMaxFlingNextView(int velocity) throws Throwable {
+ // Record the current center view.
+ View view = findCenterView(mLayoutManager);
+ assertCenterAligned(view);
+
+ int velocityDir = mReverseScroll ? -velocity : velocity;
+ mLayoutManager.expectIdleState(1);
+
+ // Scroll at one pixel in the correct direction to allow fling snapping to the next view.
+ mActivityRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mRecyclerView.scrollBy(mReverseScroll ? -1 : 1, mReverseScroll ? -1 : 1);
+ }
+ });
+ waitForIdleScroll(mRecyclerView);
+ assertTrue(fling(velocityDir, velocityDir));
+ mLayoutManager.waitForSnap(100);
+ getInstrumentation().waitForIdleSync();
+
+ View viewAfterFling = findCenterView(mLayoutManager);
+
+ assertNotSame("The view should have scrolled", view, viewAfterFling);
+ int expectedPosition = mConfig.mItemCount / 2 + (mConfig.mReverseLayout
+ ? (mReverseScroll ? 1 : -1)
+ : (mReverseScroll ? -1 : 1));
+ assertEquals(expectedPosition, mLayoutManager.getPosition(viewAfterFling));
+ assertCenterAligned(viewAfterFling);
+ }
+
+ private void setupSnapHelper() throws Throwable {
+ SnapHelper snapHelper = new PagerSnapHelper();
+ mLayoutManager.expectIdleState(1);
+ snapHelper.attachToRecyclerView(mRecyclerView);
+
+ mLayoutManager.expectLayouts(1);
+ scrollToPosition(mConfig.mItemCount / 2);
+ mLayoutManager.waitForLayout(2);
+
+ View view = findCenterView(mLayoutManager);
+ int scrollDistance = distFromCenter(view) / 2;
+ if (scrollDistance == 0) {
+ return;
+ }
+
+ int scrollDist = mReverseScroll ? -scrollDistance : scrollDistance;
+
+ mLayoutManager.expectIdleState(2);
+ smoothScrollBy(scrollDist);
+ mLayoutManager.waitForSnap(10);
+ }
+
+ @Nullable
+ private View findCenterView(RecyclerView.LayoutManager layoutManager) {
+ if (layoutManager.canScrollHorizontally()) {
+ return mRecyclerView.findChildViewUnder(mRecyclerView.getWidth() / 2, 0);
+ } else {
+ return mRecyclerView.findChildViewUnder(0, mRecyclerView.getHeight() / 2);
+ }
+ }
+
+ private int getViewDimension(View view) {
+ OrientationHelper helper;
+ if (mLayoutManager.canScrollHorizontally()) {
+ helper = OrientationHelper.createHorizontalHelper(mLayoutManager);
+ } else {
+ helper = OrientationHelper.createVerticalHelper(mLayoutManager);
+ }
+ return helper.getDecoratedMeasurement(view);
+ }
+
+ private void assertCenterAligned(View view) {
+ if (mLayoutManager.canScrollHorizontally()) {
+ assertEquals(mRecyclerView.getWidth() / 2,
+ mLayoutManager.getViewBounds(view).centerX());
+ } else {
+ assertEquals(mRecyclerView.getHeight() / 2,
+ mLayoutManager.getViewBounds(view).centerY());
+ }
+ }
+
+ private int distFromCenter(View view) {
+ if (mLayoutManager.canScrollHorizontally()) {
+ return Math.abs(mRecyclerView.getWidth() / 2
+ - mLayoutManager.getViewBounds(view).centerX());
+ } else {
+ return Math.abs(mRecyclerView.getHeight() / 2
+ - mLayoutManager.getViewBounds(view).centerY());
+ }
+ }
+
+ private boolean fling(final int velocityX, final int velocityY) throws Throwable {
+ final AtomicBoolean didStart = new AtomicBoolean(false);
+ mActivityRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ boolean result = mRecyclerView.fling(velocityX, velocityY);
+ didStart.set(result);
+ }
+ });
+ if (!didStart.get()) {
+ return false;
+ }
+ waitForIdleScroll(mRecyclerView);
+ return true;
+ }
+}