1 /*
2  * Copyright 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.recyclerview.widget;
18 
19 import android.graphics.PointF;
20 import android.util.DisplayMetrics;
21 import android.view.View;
22 
23 import androidx.annotation.NonNull;
24 import androidx.annotation.Nullable;
25 import androidx.viewpager.widget.ViewPager;
26 
27 /**
28  * Implementation of the {@link SnapHelper} supporting pager style snapping in either vertical or
29  * horizontal orientation.
30  *
31  * <p>
32  *
33  * PagerSnapHelper can help achieve a similar behavior to {@link ViewPager}.
34  * Set both {@link RecyclerView} and the items of the
35  * {@link RecyclerView.Adapter} to have
36  * {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT} height and width and then attach
37  * PagerSnapHelper to the {@link RecyclerView} using {@link #attachToRecyclerView(RecyclerView)}.
38  */
39 public class PagerSnapHelper extends SnapHelper {
40     private static final int MAX_SCROLL_ON_FLING_DURATION = 100; // ms
41 
42     // Orientation helpers are lazily created per LayoutManager.
43     @Nullable
44     private OrientationHelper mVerticalHelper;
45     @Nullable
46     private OrientationHelper mHorizontalHelper;
47 
48     @Nullable
49     @Override
calculateDistanceToFinalSnap(@onNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView)50     public int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager,
51             @NonNull View targetView) {
52         int[] out = new int[2];
53         if (layoutManager.canScrollHorizontally()) {
54             out[0] = distanceToCenter(layoutManager, targetView,
55                     getHorizontalHelper(layoutManager));
56         } else {
57             out[0] = 0;
58         }
59 
60         if (layoutManager.canScrollVertically()) {
61             out[1] = distanceToCenter(layoutManager, targetView,
62                     getVerticalHelper(layoutManager));
63         } else {
64             out[1] = 0;
65         }
66         return out;
67     }
68 
69     @Nullable
70     @Override
findSnapView(RecyclerView.LayoutManager layoutManager)71     public View findSnapView(RecyclerView.LayoutManager layoutManager) {
72         if (layoutManager.canScrollVertically()) {
73             return findCenterView(layoutManager, getVerticalHelper(layoutManager));
74         } else if (layoutManager.canScrollHorizontally()) {
75             return findCenterView(layoutManager, getHorizontalHelper(layoutManager));
76         }
77         return null;
78     }
79 
80     @Override
findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX, int velocityY)81     public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,
82             int velocityY) {
83         final int itemCount = layoutManager.getItemCount();
84         if (itemCount == 0) {
85             return RecyclerView.NO_POSITION;
86         }
87 
88         View mStartMostChildView = null;
89         if (layoutManager.canScrollVertically()) {
90             mStartMostChildView = findStartView(layoutManager, getVerticalHelper(layoutManager));
91         } else if (layoutManager.canScrollHorizontally()) {
92             mStartMostChildView = findStartView(layoutManager, getHorizontalHelper(layoutManager));
93         }
94 
95         if (mStartMostChildView == null) {
96             return RecyclerView.NO_POSITION;
97         }
98         final int centerPosition = layoutManager.getPosition(mStartMostChildView);
99         if (centerPosition == RecyclerView.NO_POSITION) {
100             return RecyclerView.NO_POSITION;
101         }
102 
103         final boolean forwardDirection;
104         if (layoutManager.canScrollHorizontally()) {
105             forwardDirection = velocityX > 0;
106         } else {
107             forwardDirection = velocityY > 0;
108         }
109         boolean reverseLayout = false;
110         if ((layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
111             RecyclerView.SmoothScroller.ScrollVectorProvider vectorProvider =
112                     (RecyclerView.SmoothScroller.ScrollVectorProvider) layoutManager;
113             PointF vectorForEnd = vectorProvider.computeScrollVectorForPosition(itemCount - 1);
114             if (vectorForEnd != null) {
115                 reverseLayout = vectorForEnd.x < 0 || vectorForEnd.y < 0;
116             }
117         }
118         return reverseLayout
119                 ? (forwardDirection ? centerPosition - 1 : centerPosition)
120                 : (forwardDirection ? centerPosition + 1 : centerPosition);
121     }
122 
123     @Override
124     protected LinearSmoothScroller createSnapScroller(RecyclerView.LayoutManager layoutManager) {
125         if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
126             return null;
127         }
128         return new LinearSmoothScroller(mRecyclerView.getContext()) {
129             @Override
130             protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
131                 int[] snapDistances = calculateDistanceToFinalSnap(mRecyclerView.getLayoutManager(),
132                         targetView);
133                 final int dx = snapDistances[0];
134                 final int dy = snapDistances[1];
135                 final int time = calculateTimeForDeceleration(Math.max(Math.abs(dx), Math.abs(dy)));
136                 if (time > 0) {
137                     action.update(dx, dy, time, mDecelerateInterpolator);
138                 }
139             }
140 
141             @Override
142             protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
143                 return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
144             }
145 
146             @Override
147             protected int calculateTimeForScrolling(int dx) {
148                 return Math.min(MAX_SCROLL_ON_FLING_DURATION, super.calculateTimeForScrolling(dx));
149             }
150         };
151     }
152 
153     private int distanceToCenter(@NonNull RecyclerView.LayoutManager layoutManager,
154             @NonNull View targetView, OrientationHelper helper) {
155         final int childCenter = helper.getDecoratedStart(targetView)
156                 + (helper.getDecoratedMeasurement(targetView) / 2);
157         final int containerCenter;
158         if (layoutManager.getClipToPadding()) {
159             containerCenter = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
160         } else {
161             containerCenter = helper.getEnd() / 2;
162         }
163         return childCenter - containerCenter;
164     }
165 
166     /**
167      * Return the child view that is currently closest to the center of this parent.
168      *
169      * @param layoutManager The {@link RecyclerView.LayoutManager} associated with the attached
170      *                      {@link RecyclerView}.
171      * @param helper The relevant {@link OrientationHelper} for the attached {@link RecyclerView}.
172      *
173      * @return the child view that is currently closest to the center of this parent.
174      */
175     @Nullable
176     private View findCenterView(RecyclerView.LayoutManager layoutManager,
177             OrientationHelper helper) {
178         int childCount = layoutManager.getChildCount();
179         if (childCount == 0) {
180             return null;
181         }
182 
183         View closestChild = null;
184         final int center;
185         if (layoutManager.getClipToPadding()) {
186             center = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
187         } else {
188             center = helper.getEnd() / 2;
189         }
190         int absClosest = Integer.MAX_VALUE;
191 
192         for (int i = 0; i < childCount; i++) {
193             final View child = layoutManager.getChildAt(i);
194             int childCenter = helper.getDecoratedStart(child)
195                     + (helper.getDecoratedMeasurement(child) / 2);
196             int absDistance = Math.abs(childCenter - center);
197 
198             /** if child center is closer than previous closest, set it as closest  **/
199             if (absDistance < absClosest) {
200                 absClosest = absDistance;
201                 closestChild = child;
202             }
203         }
204         return closestChild;
205     }
206 
207     /**
208      * Return the child view that is currently closest to the start of this parent.
209      *
210      * @param layoutManager The {@link RecyclerView.LayoutManager} associated with the attached
211      *                      {@link RecyclerView}.
212      * @param helper The relevant {@link OrientationHelper} for the attached {@link RecyclerView}.
213      *
214      * @return the child view that is currently closest to the start of this parent.
215      */
216     @Nullable
217     private View findStartView(RecyclerView.LayoutManager layoutManager,
218             OrientationHelper helper) {
219         int childCount = layoutManager.getChildCount();
220         if (childCount == 0) {
221             return null;
222         }
223 
224         View closestChild = null;
225         int startest = Integer.MAX_VALUE;
226 
227         for (int i = 0; i < childCount; i++) {
228             final View child = layoutManager.getChildAt(i);
229             int childStart = helper.getDecoratedStart(child);
230 
231             /** if child is more to start than previous closest, set it as closest  **/
232             if (childStart < startest) {
233                 startest = childStart;
234                 closestChild = child;
235             }
236         }
237         return closestChild;
238     }
239 
240     @NonNull
241     private OrientationHelper getVerticalHelper(@NonNull RecyclerView.LayoutManager layoutManager) {
242         if (mVerticalHelper == null || mVerticalHelper.mLayoutManager != layoutManager) {
243             mVerticalHelper = OrientationHelper.createVerticalHelper(layoutManager);
244         }
245         return mVerticalHelper;
246     }
247 
248     @NonNull
249     private OrientationHelper getHorizontalHelper(
250             @NonNull RecyclerView.LayoutManager layoutManager) {
251         if (mHorizontalHelper == null || mHorizontalHelper.mLayoutManager != layoutManager) {
252             mHorizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager);
253         }
254         return mHorizontalHelper;
255     }
256 }
257