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
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}
257