1b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen/*
2b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen * Copyright (C) 2017 The Android Open Source Project
3b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen *
4b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen * Licensed under the Apache License, Version 2.0 (the "License");
5b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen * you may not use this file except in compliance with the License.
6b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen * You may obtain a copy of the License at
7b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen *
8b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen *      http://www.apache.org/licenses/LICENSE-2.0
9b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen *
10b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen * Unless required by applicable law or agreed to in writing, software
11b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen * distributed under the License is distributed on an "AS IS" BASIS,
12b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen * See the License for the specific language governing permissions and
14b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen * limitations under the License.
15b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen */
16b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen
17b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chenpackage androidx.car.widget;
18b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen
19f990b0b9c0d7851e15b5eb2f5014c2f6a010cb2aAnthony Chenimport android.content.Context;
209dd755304e8ce889b858013771b8cd2b0f2499b4Anthony Chenimport android.view.View;
219dd755304e8ce889b858013771b8cd2b0f2499b4Anthony Chen
22ac5fe7c617c66850fff75a9fce9979c6e5674b0fAurimas Liutikasimport androidx.annotation.NonNull;
23ac5fe7c617c66850fff75a9fce9979c6e5674b0fAurimas Liutikasimport androidx.annotation.Nullable;
24ac5fe7c617c66850fff75a9fce9979c6e5674b0fAurimas Liutikasimport androidx.recyclerview.widget.LinearSnapHelper;
25ac5fe7c617c66850fff75a9fce9979c6e5674b0fAurimas Liutikasimport androidx.recyclerview.widget.OrientationHelper;
26ac5fe7c617c66850fff75a9fce9979c6e5674b0fAurimas Liutikasimport androidx.recyclerview.widget.RecyclerView;
27b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen
28b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen/**
29b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen * Extension of a {@link LinearSnapHelper} that will snap to the start of the target child view to
30b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen * the start of the attached {@link RecyclerView}. The start of the view is defined as the top
31b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen * if the RecyclerView is scrolling vertically; it is defined as the left (or right if RTL) if the
32b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen * RecyclerView is scrolling horizontally.
33b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen *
34b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen * <p>Snapping may be disabled for views whose height is greater than that of the
35b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen * {@code RecyclerView} that contains them. In this case, the view will only be snapped to when it
36b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen * is first encountered. Otherwise, the user will be allowed to scroll freely through that view
37b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen * when it appears in the list. The snapping behavior will resume when the large view is scrolled
38b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen * off-screen.
39b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen */
40b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chenpublic class PagedSnapHelper extends LinearSnapHelper {
41b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen    /**
42b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen     * The percentage of a View that needs to be completely visible for it to be a viable snap
43b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen     * target.
44b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen     */
45b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen    private static final float VIEW_VISIBLE_THRESHOLD = 0.5f;
46b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen
475a3a7fabf55f460b7df2b6e1694de6d6b9a2ac36Yao, Yuxing    /**
485a3a7fabf55f460b7df2b6e1694de6d6b9a2ac36Yao, Yuxing     * When a View is longer than containing RecyclerView, the percentage of the end of this View
495a3a7fabf55f460b7df2b6e1694de6d6b9a2ac36Yao, Yuxing     * that needs to be completely visible to prevent the rest of views to be a viable snap target.
505a3a7fabf55f460b7df2b6e1694de6d6b9a2ac36Yao, Yuxing     *
515a3a7fabf55f460b7df2b6e1694de6d6b9a2ac36Yao, Yuxing     * <p>In other words, if a longer-than-screen View takes more than threshold screen space on its
525a3a7fabf55f460b7df2b6e1694de6d6b9a2ac36Yao, Yuxing     * end, do not snap to any View.
535a3a7fabf55f460b7df2b6e1694de6d6b9a2ac36Yao, Yuxing     */
545a3a7fabf55f460b7df2b6e1694de6d6b9a2ac36Yao, Yuxing    private static final float LONG_ITEM_END_VISIBLE_THRESHOLD = 0.3f;
555a3a7fabf55f460b7df2b6e1694de6d6b9a2ac36Yao, Yuxing
569dd755304e8ce889b858013771b8cd2b0f2499b4Anthony Chen    private final Context mContext;
57b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen    private RecyclerView mRecyclerView;
58b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen
599dd755304e8ce889b858013771b8cd2b0f2499b4Anthony Chen    public PagedSnapHelper(Context context) {
609dd755304e8ce889b858013771b8cd2b0f2499b4Anthony Chen        mContext = context;
619dd755304e8ce889b858013771b8cd2b0f2499b4Anthony Chen    }
629dd755304e8ce889b858013771b8cd2b0f2499b4Anthony Chen
63b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen    // Orientation helpers are lazily created per LayoutManager.
64b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen    @Nullable private OrientationHelper mVerticalHelper;
65b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen    @Nullable private OrientationHelper mHorizontalHelper;
66b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen
67b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen    @Override
68b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen    public int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager,
69b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen            @NonNull View targetView) {
70b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        int[] out = new int[2];
71b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen
72b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        out[0] = layoutManager.canScrollHorizontally()
73b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen                ? getHorizontalHelper(layoutManager).getDecoratedStart(targetView)
74b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen                : 0;
75b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen
76b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        out[1] = layoutManager.canScrollVertically()
77b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen                ? getVerticalHelper(layoutManager).getDecoratedStart(targetView)
78b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen                : 0;
79b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen
80b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        return out;
81b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen    }
82b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen
83b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen    /**
84b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen     * Finds the view to snap to. The view to snap to is the child of the LayoutManager that is
85b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen     * closest to the start of the RecyclerView. The "start" depends on if the LayoutManager
86b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen     * is scrolling horizontally or vertically. If it is horizontally scrolling, then the
87b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen     * start is the view on the left (right if RTL). Otherwise, it is the top-most view.
88b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen     *
89b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen     * @param layoutManager The current {@link RecyclerView.LayoutManager} for the attached
90b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen     *                      RecyclerView.
915a3a7fabf55f460b7df2b6e1694de6d6b9a2ac36Yao, Yuxing     * @return The View closest to the start of the RecyclerView. Returns {@code null}when:
925a3a7fabf55f460b7df2b6e1694de6d6b9a2ac36Yao, Yuxing     * <ul>
935a3a7fabf55f460b7df2b6e1694de6d6b9a2ac36Yao, Yuxing     *     <li>there is no item; or
945a3a7fabf55f460b7df2b6e1694de6d6b9a2ac36Yao, Yuxing     *     <li>no visible item can fully fit in the containing RecyclerView; or
955a3a7fabf55f460b7df2b6e1694de6d6b9a2ac36Yao, Yuxing     *     <li>an item longer than containing RecyclerView is about to scroll out.
965a3a7fabf55f460b7df2b6e1694de6d6b9a2ac36Yao, Yuxing     * </ul>
97b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen     */
98b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen    @Override
99b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen    @Nullable
100b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen    public View findSnapView(RecyclerView.LayoutManager layoutManager) {
101b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        int childCount = layoutManager.getChildCount();
102b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        if (childCount == 0) {
103b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen            return null;
104b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        }
105b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen
106b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen        OrientationHelper orientationHelper = getOrientationHelper(layoutManager);
107b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen
108b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        // If there's only one child, then that will be the snap target.
109b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        if (childCount == 1) {
110b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen            View firstChild = layoutManager.getChildAt(0);
111b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen            return isValidSnapView(firstChild, orientationHelper) ? firstChild : null;
112b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        }
113b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen
1145a3a7fabf55f460b7df2b6e1694de6d6b9a2ac36Yao, Yuxing        // If the top child view is longer than the RecyclerView (long item), and it's not yet
1155a3a7fabf55f460b7df2b6e1694de6d6b9a2ac36Yao, Yuxing        // scrolled out - meaning the screen it takes up is more than threshold,
1165a3a7fabf55f460b7df2b6e1694de6d6b9a2ac36Yao, Yuxing        // do not snap to any view.
1175a3a7fabf55f460b7df2b6e1694de6d6b9a2ac36Yao, Yuxing        // This way avoids next View snapping to top "pushes" out the end of a long item.
1185a3a7fabf55f460b7df2b6e1694de6d6b9a2ac36Yao, Yuxing        View firstChild = mRecyclerView.getChildAt(0);
1195a3a7fabf55f460b7df2b6e1694de6d6b9a2ac36Yao, Yuxing        if (firstChild.getHeight() > mRecyclerView.getHeight()
1205a3a7fabf55f460b7df2b6e1694de6d6b9a2ac36Yao, Yuxing                // Long item start is scrolled past screen;
1215a3a7fabf55f460b7df2b6e1694de6d6b9a2ac36Yao, Yuxing                && orientationHelper.getDecoratedStart(firstChild) < 0
1225a3a7fabf55f460b7df2b6e1694de6d6b9a2ac36Yao, Yuxing                // and it takes up more than threshold screen size.
1235a3a7fabf55f460b7df2b6e1694de6d6b9a2ac36Yao, Yuxing                && orientationHelper.getDecoratedEnd(firstChild) > (
1245a3a7fabf55f460b7df2b6e1694de6d6b9a2ac36Yao, Yuxing                        mRecyclerView.getHeight() * LONG_ITEM_END_VISIBLE_THRESHOLD)) {
1255a3a7fabf55f460b7df2b6e1694de6d6b9a2ac36Yao, Yuxing            return null;
1265a3a7fabf55f460b7df2b6e1694de6d6b9a2ac36Yao, Yuxing        }
1275a3a7fabf55f460b7df2b6e1694de6d6b9a2ac36Yao, Yuxing
128b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        View lastVisibleChild = layoutManager.getChildAt(childCount - 1);
129b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen
130b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        // Check if the last child visible is the last item in the list.
131b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        boolean lastItemVisible =
132b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen                layoutManager.getPosition(lastVisibleChild) == layoutManager.getItemCount() - 1;
133b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen
134b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        // If it is, then check how much of that view is visible.
135b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        float lastItemPercentageVisible = lastItemVisible
136b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen                ? getPercentageVisible(lastVisibleChild, orientationHelper) : 0;
137b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen
138b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        View closestChild = null;
139b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        int closestDistanceToStart = Integer.MAX_VALUE;
140b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        float closestPercentageVisible = 0.f;
141b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen
142b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen        // Iterate to find the child closest to the top and more than half way visible.
143b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        for (int i = 0; i < childCount; i++) {
144b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen            View child = layoutManager.getChildAt(i);
145b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen            int startOffset = orientationHelper.getDecoratedStart(child);
146b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen
147b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen            if (Math.abs(startOffset) < closestDistanceToStart) {
148b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen                float percentageVisible = getPercentageVisible(child, orientationHelper);
149b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen
150b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen                if (percentageVisible > VIEW_VISIBLE_THRESHOLD
151b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen                        && percentageVisible > closestPercentageVisible) {
152b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen                    closestDistanceToStart = startOffset;
153b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen                    closestChild = child;
154b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen                    closestPercentageVisible = percentageVisible;
155b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen                }
156b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen            }
157b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        }
158b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen
159b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen        View childToReturn = closestChild;
160b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen
161b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen        // If closestChild is null, then that means we were unable to find a closest child that
162b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen        // is over the VIEW_VISIBLE_THRESHOLD. This could happen if the views are larger than
163b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen        // the given area. In this case, consider returning the lastVisibleChild so that the screen
164b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen        // scrolls. Also, check if the last item should be displayed anyway if it is mostly visible.
165b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen        if ((childToReturn == null
166b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen                || (lastItemVisible && lastItemPercentageVisible > closestPercentageVisible))) {
167b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen            childToReturn = lastVisibleChild;
168b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen        }
169b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen
170b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen        // Return null if the childToReturn is not valid. This allows the user to scroll freely
171b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen        // with no snapping. This can allow them to see the entire view.
172b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen        return isValidSnapView(childToReturn, orientationHelper) ? childToReturn : null;
173b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen    }
174b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen
175b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen    /**
176b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen     * Returns whether or not the given View is a valid snapping view. A view is considered valid
1775a3a7fabf55f460b7df2b6e1694de6d6b9a2ac36Yao, Yuxing     * for snapping if it can fit entirely within the height of the RecyclerView it is contained
1785a3a7fabf55f460b7df2b6e1694de6d6b9a2ac36Yao, Yuxing     * within.
179b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen     *
180b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen     * <p>If the view is larger than the RecyclerView, then it might not want to be snapped to
1815a3a7fabf55f460b7df2b6e1694de6d6b9a2ac36Yao, Yuxing     * to allow the user to scroll and see the rest of the View.
182b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen     *
183b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen     * @param view The view to determine the snapping potential.
184b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen     * @param helper The {@link OrientationHelper} associated with the current RecyclerView.
185b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen     * @return {@code true} if the given view is a valid snapping view; {@code false} otherwise.
186b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen     */
187b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen    private boolean isValidSnapView(View view, OrientationHelper helper) {
1885a3a7fabf55f460b7df2b6e1694de6d6b9a2ac36Yao, Yuxing        return helper.getDecoratedMeasurement(view) <= helper.getLayoutManager().getHeight();
189b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen    }
190b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen
191b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen    /**
192b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen     * Returns the percentage of the given view that is visible, relative to its containing
193b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen     * RecyclerView.
194b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen     *
195b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen     * @param view The View to get the percentage visible of.
196b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen     * @param helper An {@link OrientationHelper} to aid with calculation.
197b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen     * @return A float indicating the percentage of the given view that is visible.
198b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen     */
199b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen    private float getPercentageVisible(View view, OrientationHelper helper) {
200b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        int start = 0;
201b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        int end = helper.getEnd();
202b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen
203b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        int viewStart = helper.getDecoratedStart(view);
204b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        int viewEnd = helper.getDecoratedEnd(view);
205b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen
206b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        if (viewStart >= start && viewEnd <= end) {
207b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen            // The view is within the bounds of the RecyclerView, so it's fully visible.
208b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen            return 1.f;
209b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        } else if (viewStart <= start && viewEnd >= end) {
210b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen            // The view is larger than the height of the RecyclerView.
211b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen            int viewHeight = helper.getDecoratedMeasurement(view);
212b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen            return 1.f - ((float) (Math.abs(viewStart) + Math.abs(viewEnd)) / viewHeight);
213b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        } else if (viewStart < start) {
214b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen            // The view is above the start of the RecyclerView, so subtract the start offset
215b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen            // from the total height.
216b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen            return 1.f - ((float) Math.abs(viewStart) / helper.getDecoratedMeasurement(view));
217b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        } else {
218b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen            // The view is below the end of the RecyclerView, so subtract the end offset from the
219b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen            // total height.
220b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen            return 1.f - ((float) Math.abs(viewEnd) / helper.getDecoratedMeasurement(view));
221b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        }
222b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen    }
223b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen
224b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen    @Override
225b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen    public void attachToRecyclerView(@Nullable RecyclerView recyclerView) {
226b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        super.attachToRecyclerView(recyclerView);
227b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        mRecyclerView = recyclerView;
228b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen    }
229b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen
230b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen    /**
231f990b0b9c0d7851e15b5eb2f5014c2f6a010cb2aAnthony Chen     * Returns a scroller specific to this {@code PagedSnapHelper}. This scroller is used for all
232f990b0b9c0d7851e15b5eb2f5014c2f6a010cb2aAnthony Chen     * smooth scrolling operations, including flings.
233f990b0b9c0d7851e15b5eb2f5014c2f6a010cb2aAnthony Chen     *
234f990b0b9c0d7851e15b5eb2f5014c2f6a010cb2aAnthony Chen     * @param layoutManager The {@link RecyclerView.LayoutManager} associated with the attached
235f990b0b9c0d7851e15b5eb2f5014c2f6a010cb2aAnthony Chen     *                      {@link RecyclerView}.
236f990b0b9c0d7851e15b5eb2f5014c2f6a010cb2aAnthony Chen     *
237f990b0b9c0d7851e15b5eb2f5014c2f6a010cb2aAnthony Chen     * @return a {@link RecyclerView.SmoothScroller} which will handle the scrolling.
238f990b0b9c0d7851e15b5eb2f5014c2f6a010cb2aAnthony Chen     */
239f990b0b9c0d7851e15b5eb2f5014c2f6a010cb2aAnthony Chen    @Override
240f990b0b9c0d7851e15b5eb2f5014c2f6a010cb2aAnthony Chen    protected RecyclerView.SmoothScroller createScroller(RecyclerView.LayoutManager layoutManager) {
2419dd755304e8ce889b858013771b8cd2b0f2499b4Anthony Chen        return new PagedSmoothScroller(mContext);
242f990b0b9c0d7851e15b5eb2f5014c2f6a010cb2aAnthony Chen    }
243f990b0b9c0d7851e15b5eb2f5014c2f6a010cb2aAnthony Chen
244f990b0b9c0d7851e15b5eb2f5014c2f6a010cb2aAnthony Chen    /**
245b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen     * Calculate the estimated scroll distance in each direction given velocities on both axes.
246b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen     * This method will clamp the maximum scroll distance so that a single fling will never scroll
247b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen     * more than one page.
248b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen     *
249b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen     * @param velocityX Fling velocity on the horizontal axis.
250b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen     * @param velocityY Fling velocity on the vertical axis.
251b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen     * @return An array holding the calculated distances in x and y directions respectively.
252b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen     */
253b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen    @Override
254b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen    public int[] calculateScrollDistance(int velocityX, int velocityY) {
255b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        int[] outDist = super.calculateScrollDistance(velocityX, velocityY);
256b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen
257b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        if (mRecyclerView == null) {
258b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen            return outDist;
259b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        }
260b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen
261b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
262b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        if (layoutManager == null || layoutManager.getChildCount() == 0) {
263b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen            return outDist;
264b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        }
265b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen
266b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        int lastChildPosition = isAtEnd(layoutManager) ? 0 : layoutManager.getChildCount() - 1;
267b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen
268b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen        OrientationHelper orientationHelper = getOrientationHelper(layoutManager);
269b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen        View lastChild = layoutManager.getChildAt(lastChildPosition);
270b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen        float percentageVisible = getPercentageVisible(lastChild, orientationHelper);
271b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen
272b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen        int maxDistance = layoutManager.getHeight();
273b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen        if (percentageVisible > 0.f) {
274b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen            // The max and min distance is the total height of the RecyclerView minus the height of
275b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen            // the last child. This ensures that each scroll will never scroll more than a single
276b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen            // page on the RecyclerView. That is, the max scroll will make the last child the
277b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen            // first child and vice versa when scrolling the opposite way.
278b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen            maxDistance -= layoutManager.getDecoratedMeasuredHeight(lastChild);
279b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen        }
280b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen
281b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        int minDistance = -maxDistance;
282b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen
283b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        outDist[0] = clamp(outDist[0], minDistance, maxDistance);
284b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        outDist[1] = clamp(outDist[1], minDistance, maxDistance);
285b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen
286b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        return outDist;
287b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen    }
288b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen
289b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen    /** Returns {@code true} if the RecyclerView is completely displaying the first item. */
290b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen    public boolean isAtStart(RecyclerView.LayoutManager layoutManager) {
291b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        if (layoutManager == null || layoutManager.getChildCount() == 0) {
292b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen            return true;
293b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        }
294b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen
295b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        View firstChild = layoutManager.getChildAt(0);
296b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        OrientationHelper orientationHelper = layoutManager.canScrollVertically()
297b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen                ? getVerticalHelper(layoutManager)
298b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen                : getHorizontalHelper(layoutManager);
299b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen
300b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        // Check that the first child is completely visible and is the first item in the list.
301b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        return orientationHelper.getDecoratedStart(firstChild) >= 0
302b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen                && layoutManager.getPosition(firstChild) == 0;
303b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen    }
304b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen
305b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen    /** Returns {@code true} if the RecyclerView is completely displaying the last item. */
306b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen    public boolean isAtEnd(RecyclerView.LayoutManager layoutManager) {
307b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        if (layoutManager == null || layoutManager.getChildCount() == 0) {
308b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen            return true;
309b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        }
310b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen
311b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        int childCount = layoutManager.getChildCount();
312b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        View lastVisibleChild = layoutManager.getChildAt(childCount - 1);
313b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen
314b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        // The list has reached the bottom if the last child that is visible is the last item
315b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        // in the list and it's fully shown.
316b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        return layoutManager.getPosition(lastVisibleChild) == (layoutManager.getItemCount() - 1)
317b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen                && layoutManager.getDecoratedBottom(lastVisibleChild) <= layoutManager.getHeight();
318b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen    }
319b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen
320b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen    /**
321b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen     * Returns an {@link OrientationHelper} that corresponds to the current scroll direction of
322ac5fe7c617c66850fff75a9fce9979c6e5674b0fAurimas Liutikas     * the given {@link RecyclerView.LayoutManager}.
323b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen     */
324b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen    @NonNull
325b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen    private OrientationHelper getOrientationHelper(
326b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen            @NonNull RecyclerView.LayoutManager layoutManager) {
327b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen        return layoutManager.canScrollVertically()
328b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen                ? getVerticalHelper(layoutManager)
329b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen                : getHorizontalHelper(layoutManager);
330b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen    }
331b1df1da479733666c3c877ff3fdc602f9d3f271aAnthony Chen
332b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen    @NonNull
333b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen    private OrientationHelper getVerticalHelper(@NonNull RecyclerView.LayoutManager layoutManager) {
334b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        if (mVerticalHelper == null || mVerticalHelper.getLayoutManager() != layoutManager) {
335b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen            mVerticalHelper = OrientationHelper.createVerticalHelper(layoutManager);
336b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        }
337b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        return mVerticalHelper;
338b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen    }
339b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen
340b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen    @NonNull
341b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen    private OrientationHelper getHorizontalHelper(
342b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen            @NonNull RecyclerView.LayoutManager layoutManager) {
343b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        if (mHorizontalHelper == null || mHorizontalHelper.getLayoutManager() != layoutManager) {
344b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen            mHorizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager);
345b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        }
346b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        return mHorizontalHelper;
347b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen    }
348b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen
349b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen    /**
350b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen     * Ensures that the given value falls between the range given by the min and max values. This
351b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen     * method does not check that the min value is greater than or equal to the max value. If the
352b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen     * parameters are not well-formed, this method's behavior is undefined.
353b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen     *
354b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen     * @param value The value to clamp.
355b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen     * @param min The minimum value the given value can be.
356b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen     * @param max The maximum value the given value can be.
357b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen     * @return A number that falls between {@code min} or {@code max} or one of those values if the
358b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen     * given value is less than or greater than {@code min} and {@code max} respectively.
359b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen     */
360b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen    private static int clamp(int value, int min, int max) {
361b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen        return Math.max(min, Math.min(max, value));
362b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen    }
363b226d4dcc396fe8837876dfa113143702dd23c26Anthony Chen}
364