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