PagerSnapHelper.java revision 849542dfdc5e83411c8b959251eb6f2a1556fc9d
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
17package android.support.v7.widget;
18
19import android.graphics.PointF;
20import android.support.annotation.NonNull;
21import android.support.annotation.Nullable;
22import android.view.View;
23
24/**
25 * Implementation of the {@link SnapHelper} supporting pager style snapping in either vertical or
26 * horizontal orientation.
27 */
28public class PagerSnapHelper extends SnapHelper {
29    // Orientation helpers are lazily created per LayoutManager.
30    @Nullable
31    private OrientationHelper mVerticalHelper;
32    @Nullable
33    private OrientationHelper mHorizontalHelper;
34
35    @Nullable
36    @Override
37    public int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager,
38            @NonNull View targetView) {
39        int[] out = new int[2];
40        if (layoutManager.canScrollHorizontally()) {
41            out[0] = distanceToCenter(layoutManager, targetView,
42                    getHorizontalHelper(layoutManager));
43        } else {
44            out[0] = 0;
45        }
46
47        if (layoutManager.canScrollVertically()) {
48            out[1] = distanceToCenter(layoutManager, targetView,
49                    getVerticalHelper(layoutManager));
50        } else {
51            out[1] = 0;
52        }
53        return out;
54    }
55
56    @Nullable
57    @Override
58    public View findSnapView(RecyclerView.LayoutManager layoutManager) {
59        if (layoutManager.canScrollVertically()) {
60            return findCenterView(layoutManager, getVerticalHelper(layoutManager));
61        } else if (layoutManager.canScrollHorizontally()) {
62            return findCenterView(layoutManager, getHorizontalHelper(layoutManager));
63        }
64        return null;
65    }
66
67    @Override
68    public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,
69            int velocityY) {
70        final int itemCount = layoutManager.getItemCount();
71        if (itemCount == 0) {
72            return RecyclerView.NO_POSITION;
73        }
74
75        View mStartMostChildView = null;
76        if (layoutManager.canScrollVertically()) {
77            mStartMostChildView = findStartView(layoutManager, getVerticalHelper(layoutManager));
78        } else if (layoutManager.canScrollHorizontally()) {
79            mStartMostChildView = findStartView(layoutManager, getHorizontalHelper(layoutManager));
80        }
81
82        if (mStartMostChildView == null) {
83            return RecyclerView.NO_POSITION;
84        }
85        final int centerPosition = layoutManager.getPosition(mStartMostChildView);
86        if (centerPosition == RecyclerView.NO_POSITION) {
87            return RecyclerView.NO_POSITION;
88        }
89
90        final boolean forwardDirection;
91        if (layoutManager.canScrollHorizontally()) {
92            forwardDirection = velocityX > 0;
93        } else {
94            forwardDirection = velocityY > 0;
95        }
96        boolean reverseLayout = false;
97        if ((layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
98            RecyclerView.SmoothScroller.ScrollVectorProvider vectorProvider =
99                    (RecyclerView.SmoothScroller.ScrollVectorProvider) layoutManager;
100            PointF vectorForEnd = vectorProvider.computeScrollVectorForPosition(itemCount - 1);
101            if (vectorForEnd != null) {
102                reverseLayout = vectorForEnd.x < 0 || vectorForEnd.y < 0;
103            }
104        }
105        return reverseLayout
106                ? (forwardDirection ? centerPosition - 1 : centerPosition)
107                : (forwardDirection ? centerPosition + 1 : centerPosition);
108    }
109
110    private int distanceToCenter(@NonNull RecyclerView.LayoutManager layoutManager,
111            @NonNull View targetView, OrientationHelper helper) {
112        final int childCenter = helper.getDecoratedStart(targetView)
113                + (helper.getDecoratedMeasurement(targetView) / 2);
114        final int containerCenter;
115        if (layoutManager.getClipToPadding()) {
116            containerCenter = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
117        } else {
118            containerCenter = helper.getEnd() / 2;
119        }
120        return childCenter - containerCenter;
121    }
122
123    /**
124     * Return the child view that is currently closest to the center of this parent.
125     *
126     * @param layoutManager The {@link RecyclerView.LayoutManager} associated with the attached
127     *                      {@link RecyclerView}.
128     * @param helper The relevant {@link OrientationHelper} for the attached {@link RecyclerView}.
129     *
130     * @return the child view that is currently closest to the center of this parent.
131     */
132    @Nullable
133    private View findCenterView(RecyclerView.LayoutManager layoutManager,
134            OrientationHelper helper) {
135        int childCount = layoutManager.getChildCount();
136        if (childCount == 0) {
137            return null;
138        }
139
140        View closestChild = null;
141        final int center;
142        if (layoutManager.getClipToPadding()) {
143            center = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
144        } else {
145            center = helper.getEnd() / 2;
146        }
147        int absClosest = Integer.MAX_VALUE;
148
149        for (int i = 0; i < childCount; i++) {
150            final View child = layoutManager.getChildAt(i);
151            int childCenter = helper.getDecoratedStart(child)
152                    + (helper.getDecoratedMeasurement(child) / 2);
153            int absDistance = Math.abs(childCenter - center);
154
155            /** if child center is closer than previous closest, set it as closest  **/
156            if (absDistance < absClosest) {
157                absClosest = absDistance;
158                closestChild = child;
159            }
160        }
161        return closestChild;
162    }
163
164    /**
165     * Return the child view that is currently closest to the start 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 start of this parent.
172     */
173    @Nullable
174    private View findStartView(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        int startest = Integer.MAX_VALUE;
183
184        for (int i = 0; i < childCount; i++) {
185            final View child = layoutManager.getChildAt(i);
186            int childStart = helper.getDecoratedStart(child);
187
188            /** if child is more to start than previous closest, set it as closest  **/
189            if (childStart < startest) {
190                startest = childStart;
191                closestChild = child;
192            }
193        }
194        return closestChild;
195    }
196
197    @NonNull
198    private OrientationHelper getVerticalHelper(@NonNull RecyclerView.LayoutManager layoutManager) {
199        if (mVerticalHelper == null || mVerticalHelper.mLayoutManager != layoutManager) {
200            mVerticalHelper = OrientationHelper.createVerticalHelper(layoutManager);
201        }
202        return mVerticalHelper;
203    }
204
205    @NonNull
206    private OrientationHelper getHorizontalHelper(
207            @NonNull RecyclerView.LayoutManager layoutManager) {
208        if (mHorizontalHelper == null || mHorizontalHelper.mLayoutManager != layoutManager) {
209            mHorizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager);
210        }
211        return mHorizontalHelper;
212    }
213}
214