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 snapping in either vertical or horizontal 26 * orientation. 27 * <p> 28 * The implementation will snap the center of the target child view to the center of 29 * the attached {@link RecyclerView}. If you intend to change this behavior then override 30 * {@link SnapHelper#calculateDistanceToFinalSnap}. 31 */ 32public class LinearSnapHelper extends SnapHelper { 33 34 private static final float INVALID_DISTANCE = 1f; 35 36 // Orientation helpers are lazily created per LayoutManager. 37 @Nullable 38 private OrientationHelper mVerticalHelper; 39 @Nullable 40 private OrientationHelper mHorizontalHelper; 41 42 @Override 43 public int[] calculateDistanceToFinalSnap( 44 @NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView) { 45 int[] out = new int[2]; 46 if (layoutManager.canScrollHorizontally()) { 47 out[0] = distanceToCenter(layoutManager, targetView, 48 getHorizontalHelper(layoutManager)); 49 } else { 50 out[0] = 0; 51 } 52 53 if (layoutManager.canScrollVertically()) { 54 out[1] = distanceToCenter(layoutManager, targetView, 55 getVerticalHelper(layoutManager)); 56 } else { 57 out[1] = 0; 58 } 59 return out; 60 } 61 62 @Override 63 public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX, 64 int velocityY) { 65 if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) { 66 return RecyclerView.NO_POSITION; 67 } 68 69 final int itemCount = layoutManager.getItemCount(); 70 if (itemCount == 0) { 71 return RecyclerView.NO_POSITION; 72 } 73 74 final View currentView = findSnapView(layoutManager); 75 if (currentView == null) { 76 return RecyclerView.NO_POSITION; 77 } 78 79 final int currentPosition = layoutManager.getPosition(currentView); 80 if (currentPosition == RecyclerView.NO_POSITION) { 81 return RecyclerView.NO_POSITION; 82 } 83 84 RecyclerView.SmoothScroller.ScrollVectorProvider vectorProvider = 85 (RecyclerView.SmoothScroller.ScrollVectorProvider) layoutManager; 86 // deltaJumps sign comes from the velocity which may not match the order of children in 87 // the LayoutManager. To overcome this, we ask for a vector from the LayoutManager to 88 // get the direction. 89 PointF vectorForEnd = vectorProvider.computeScrollVectorForPosition(itemCount - 1); 90 if (vectorForEnd == null) { 91 // cannot get a vector for the given position. 92 return RecyclerView.NO_POSITION; 93 } 94 95 int vDeltaJump, hDeltaJump; 96 if (layoutManager.canScrollHorizontally()) { 97 hDeltaJump = estimateNextPositionDiffForFling(layoutManager, 98 getHorizontalHelper(layoutManager), velocityX, 0); 99 if (vectorForEnd.x < 0) { 100 hDeltaJump = -hDeltaJump; 101 } 102 } else { 103 hDeltaJump = 0; 104 } 105 if (layoutManager.canScrollVertically()) { 106 vDeltaJump = estimateNextPositionDiffForFling(layoutManager, 107 getVerticalHelper(layoutManager), 0, velocityY); 108 if (vectorForEnd.y < 0) { 109 vDeltaJump = -vDeltaJump; 110 } 111 } else { 112 vDeltaJump = 0; 113 } 114 115 int deltaJump = layoutManager.canScrollVertically() ? vDeltaJump : hDeltaJump; 116 if (deltaJump == 0) { 117 return RecyclerView.NO_POSITION; 118 } 119 120 int targetPos = currentPosition + deltaJump; 121 if (targetPos < 0) { 122 targetPos = 0; 123 } 124 if (targetPos >= itemCount) { 125 targetPos = itemCount - 1; 126 } 127 return targetPos; 128 } 129 130 @Override 131 public View findSnapView(RecyclerView.LayoutManager layoutManager) { 132 if (layoutManager.canScrollVertically()) { 133 return findCenterView(layoutManager, getVerticalHelper(layoutManager)); 134 } else if (layoutManager.canScrollHorizontally()) { 135 return findCenterView(layoutManager, getHorizontalHelper(layoutManager)); 136 } 137 return null; 138 } 139 140 private int distanceToCenter(@NonNull RecyclerView.LayoutManager layoutManager, 141 @NonNull View targetView, OrientationHelper helper) { 142 final int childCenter = helper.getDecoratedStart(targetView) 143 + (helper.getDecoratedMeasurement(targetView) / 2); 144 final int containerCenter; 145 if (layoutManager.getClipToPadding()) { 146 containerCenter = helper.getStartAfterPadding() + helper.getTotalSpace() / 2; 147 } else { 148 containerCenter = helper.getEnd() / 2; 149 } 150 return childCenter - containerCenter; 151 } 152 153 /** 154 * Estimates a position to which SnapHelper will try to scroll to in response to a fling. 155 * 156 * @param layoutManager The {@link RecyclerView.LayoutManager} associated with the attached 157 * {@link RecyclerView}. 158 * @param helper The {@link OrientationHelper} that is created from the LayoutManager. 159 * @param velocityX The velocity on the x axis. 160 * @param velocityY The velocity on the y axis. 161 * 162 * @return The diff between the target scroll position and the current position. 163 */ 164 private int estimateNextPositionDiffForFling(RecyclerView.LayoutManager layoutManager, 165 OrientationHelper helper, int velocityX, int velocityY) { 166 int[] distances = calculateScrollDistance(velocityX, velocityY); 167 float distancePerChild = computeDistancePerChild(layoutManager, helper); 168 if (distancePerChild <= 0) { 169 return 0; 170 } 171 int distance = 172 Math.abs(distances[0]) > Math.abs(distances[1]) ? distances[0] : distances[1]; 173 return (int) Math.round(distance / distancePerChild); 174 } 175 176 /** 177 * Return the child view that is currently closest to the center of this parent. 178 * 179 * @param layoutManager The {@link RecyclerView.LayoutManager} associated with the attached 180 * {@link RecyclerView}. 181 * @param helper The relevant {@link OrientationHelper} for the attached {@link RecyclerView}. 182 * 183 * @return the child view that is currently closest to the center of this parent. 184 */ 185 @Nullable 186 private View findCenterView(RecyclerView.LayoutManager layoutManager, 187 OrientationHelper helper) { 188 int childCount = layoutManager.getChildCount(); 189 if (childCount == 0) { 190 return null; 191 } 192 193 View closestChild = null; 194 final int center; 195 if (layoutManager.getClipToPadding()) { 196 center = helper.getStartAfterPadding() + helper.getTotalSpace() / 2; 197 } else { 198 center = helper.getEnd() / 2; 199 } 200 int absClosest = Integer.MAX_VALUE; 201 202 for (int i = 0; i < childCount; i++) { 203 final View child = layoutManager.getChildAt(i); 204 int childCenter = helper.getDecoratedStart(child) 205 + (helper.getDecoratedMeasurement(child) / 2); 206 int absDistance = Math.abs(childCenter - center); 207 208 /** if child center is closer than previous closest, set it as closest **/ 209 if (absDistance < absClosest) { 210 absClosest = absDistance; 211 closestChild = child; 212 } 213 } 214 return closestChild; 215 } 216 217 /** 218 * Computes an average pixel value to pass a single child. 219 * <p> 220 * Returns a negative value if it cannot be calculated. 221 * 222 * @param layoutManager The {@link RecyclerView.LayoutManager} associated with the attached 223 * {@link RecyclerView}. 224 * @param helper The relevant {@link OrientationHelper} for the attached 225 * {@link RecyclerView.LayoutManager}. 226 * 227 * @return A float value that is the average number of pixels needed to scroll by one view in 228 * the relevant direction. 229 */ 230 private float computeDistancePerChild(RecyclerView.LayoutManager layoutManager, 231 OrientationHelper helper) { 232 View minPosView = null; 233 View maxPosView = null; 234 int minPos = Integer.MAX_VALUE; 235 int maxPos = Integer.MIN_VALUE; 236 int childCount = layoutManager.getChildCount(); 237 if (childCount == 0) { 238 return INVALID_DISTANCE; 239 } 240 241 for (int i = 0; i < childCount; i++) { 242 View child = layoutManager.getChildAt(i); 243 final int pos = layoutManager.getPosition(child); 244 if (pos == RecyclerView.NO_POSITION) { 245 continue; 246 } 247 if (pos < minPos) { 248 minPos = pos; 249 minPosView = child; 250 } 251 if (pos > maxPos) { 252 maxPos = pos; 253 maxPosView = child; 254 } 255 } 256 if (minPosView == null || maxPosView == null) { 257 return INVALID_DISTANCE; 258 } 259 int start = Math.min(helper.getDecoratedStart(minPosView), 260 helper.getDecoratedStart(maxPosView)); 261 int end = Math.max(helper.getDecoratedEnd(minPosView), 262 helper.getDecoratedEnd(maxPosView)); 263 int distance = end - start; 264 if (distance == 0) { 265 return INVALID_DISTANCE; 266 } 267 return 1f * distance / ((maxPos - minPos) + 1); 268 } 269 270 @NonNull 271 private OrientationHelper getVerticalHelper(@NonNull RecyclerView.LayoutManager layoutManager) { 272 if (mVerticalHelper == null || mVerticalHelper.mLayoutManager != layoutManager) { 273 mVerticalHelper = OrientationHelper.createVerticalHelper(layoutManager); 274 } 275 return mVerticalHelper; 276 } 277 278 @NonNull 279 private OrientationHelper getHorizontalHelper( 280 @NonNull RecyclerView.LayoutManager layoutManager) { 281 if (mHorizontalHelper == null || mHorizontalHelper.mLayoutManager != layoutManager) { 282 mHorizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager); 283 } 284 return mHorizontalHelper; 285 } 286} 287