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