blob: d162e528205c053c446c32da65c4f53d1fabaa1f [file] [log] [blame]
Justin Klaassen4d01eea2018-04-03 23:21:57 -04001/*
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
17package androidx.recyclerview.widget;
18
19import android.graphics.PointF;
20import android.util.DisplayMetrics;
21import android.view.View;
22
23import androidx.annotation.NonNull;
24import androidx.annotation.Nullable;
25import 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 */
39public 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
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
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
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}