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 17 package android.support.v7.widget; 18 19 import android.graphics.PointF; 20 import android.support.annotation.NonNull; 21 import android.support.annotation.Nullable; 22 import android.util.DisplayMetrics; 23 import android.view.View; 24 25 /** 26 * Implementation of the {@link SnapHelper} supporting pager style snapping in either vertical or 27 * horizontal orientation. 28 * 29 * <p> 30 * 31 * PagerSnapHelper can help achieve a similar behavior to {@link android.support.v4.view.ViewPager}. 32 * Set both {@link RecyclerView} and the items of the 33 * {@link android.support.v7.widget.RecyclerView.Adapter} to have 34 * {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT} height and width and then attach 35 * PagerSnapHelper to the {@link RecyclerView} using {@link #attachToRecyclerView(RecyclerView)}. 36 */ 37 public class PagerSnapHelper extends SnapHelper { 38 private static final int MAX_SCROLL_ON_FLING_DURATION = 100; // ms 39 40 // Orientation helpers are lazily created per LayoutManager. 41 @Nullable 42 private OrientationHelper mVerticalHelper; 43 @Nullable 44 private OrientationHelper mHorizontalHelper; 45 46 @Nullable 47 @Override calculateDistanceToFinalSnap(@onNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView)48 public int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager, 49 @NonNull View targetView) { 50 int[] out = new int[2]; 51 if (layoutManager.canScrollHorizontally()) { 52 out[0] = distanceToCenter(layoutManager, targetView, 53 getHorizontalHelper(layoutManager)); 54 } else { 55 out[0] = 0; 56 } 57 58 if (layoutManager.canScrollVertically()) { 59 out[1] = distanceToCenter(layoutManager, targetView, 60 getVerticalHelper(layoutManager)); 61 } else { 62 out[1] = 0; 63 } 64 return out; 65 } 66 67 @Nullable 68 @Override findSnapView(RecyclerView.LayoutManager layoutManager)69 public View findSnapView(RecyclerView.LayoutManager layoutManager) { 70 if (layoutManager.canScrollVertically()) { 71 return findCenterView(layoutManager, getVerticalHelper(layoutManager)); 72 } else if (layoutManager.canScrollHorizontally()) { 73 return findCenterView(layoutManager, getHorizontalHelper(layoutManager)); 74 } 75 return null; 76 } 77 78 @Override findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX, int velocityY)79 public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX, 80 int velocityY) { 81 final int itemCount = layoutManager.getItemCount(); 82 if (itemCount == 0) { 83 return RecyclerView.NO_POSITION; 84 } 85 86 View mStartMostChildView = null; 87 if (layoutManager.canScrollVertically()) { 88 mStartMostChildView = findStartView(layoutManager, getVerticalHelper(layoutManager)); 89 } else if (layoutManager.canScrollHorizontally()) { 90 mStartMostChildView = findStartView(layoutManager, getHorizontalHelper(layoutManager)); 91 } 92 93 if (mStartMostChildView == null) { 94 return RecyclerView.NO_POSITION; 95 } 96 final int centerPosition = layoutManager.getPosition(mStartMostChildView); 97 if (centerPosition == RecyclerView.NO_POSITION) { 98 return RecyclerView.NO_POSITION; 99 } 100 101 final boolean forwardDirection; 102 if (layoutManager.canScrollHorizontally()) { 103 forwardDirection = velocityX > 0; 104 } else { 105 forwardDirection = velocityY > 0; 106 } 107 boolean reverseLayout = false; 108 if ((layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) { 109 RecyclerView.SmoothScroller.ScrollVectorProvider vectorProvider = 110 (RecyclerView.SmoothScroller.ScrollVectorProvider) layoutManager; 111 PointF vectorForEnd = vectorProvider.computeScrollVectorForPosition(itemCount - 1); 112 if (vectorForEnd != null) { 113 reverseLayout = vectorForEnd.x < 0 || vectorForEnd.y < 0; 114 } 115 } 116 return reverseLayout 117 ? (forwardDirection ? centerPosition - 1 : centerPosition) 118 : (forwardDirection ? centerPosition + 1 : centerPosition); 119 } 120 121 @Override 122 protected LinearSmoothScroller createSnapScroller(RecyclerView.LayoutManager layoutManager) { 123 if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) { 124 return null; 125 } 126 return new LinearSmoothScroller(mRecyclerView.getContext()) { 127 @Override 128 protected void onTargetFound(View targetView, RecyclerView.State state, Action action) { 129 int[] snapDistances = calculateDistanceToFinalSnap(mRecyclerView.getLayoutManager(), 130 targetView); 131 final int dx = snapDistances[0]; 132 final int dy = snapDistances[1]; 133 final int time = calculateTimeForDeceleration(Math.max(Math.abs(dx), Math.abs(dy))); 134 if (time > 0) { 135 action.update(dx, dy, time, mDecelerateInterpolator); 136 } 137 } 138 139 @Override 140 protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) { 141 return MILLISECONDS_PER_INCH / displayMetrics.densityDpi; 142 } 143 144 @Override 145 protected int calculateTimeForScrolling(int dx) { 146 return Math.min(MAX_SCROLL_ON_FLING_DURATION, super.calculateTimeForScrolling(dx)); 147 } 148 }; 149 } 150 151 private int distanceToCenter(@NonNull RecyclerView.LayoutManager layoutManager, 152 @NonNull View targetView, OrientationHelper helper) { 153 final int childCenter = helper.getDecoratedStart(targetView) 154 + (helper.getDecoratedMeasurement(targetView) / 2); 155 final int containerCenter; 156 if (layoutManager.getClipToPadding()) { 157 containerCenter = helper.getStartAfterPadding() + helper.getTotalSpace() / 2; 158 } else { 159 containerCenter = helper.getEnd() / 2; 160 } 161 return childCenter - containerCenter; 162 } 163 164 /** 165 * Return the child view that is currently closest to the center 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 center of this parent. 172 */ 173 @Nullable 174 private View findCenterView(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 final int center; 183 if (layoutManager.getClipToPadding()) { 184 center = helper.getStartAfterPadding() + helper.getTotalSpace() / 2; 185 } else { 186 center = helper.getEnd() / 2; 187 } 188 int absClosest = Integer.MAX_VALUE; 189 190 for (int i = 0; i < childCount; i++) { 191 final View child = layoutManager.getChildAt(i); 192 int childCenter = helper.getDecoratedStart(child) 193 + (helper.getDecoratedMeasurement(child) / 2); 194 int absDistance = Math.abs(childCenter - center); 195 196 /** if child center is closer than previous closest, set it as closest **/ 197 if (absDistance < absClosest) { 198 absClosest = absDistance; 199 closestChild = child; 200 } 201 } 202 return closestChild; 203 } 204 205 /** 206 * Return the child view that is currently closest to the start of this parent. 207 * 208 * @param layoutManager The {@link RecyclerView.LayoutManager} associated with the attached 209 * {@link RecyclerView}. 210 * @param helper The relevant {@link OrientationHelper} for the attached {@link RecyclerView}. 211 * 212 * @return the child view that is currently closest to the start of this parent. 213 */ 214 @Nullable 215 private View findStartView(RecyclerView.LayoutManager layoutManager, 216 OrientationHelper helper) { 217 int childCount = layoutManager.getChildCount(); 218 if (childCount == 0) { 219 return null; 220 } 221 222 View closestChild = null; 223 int startest = Integer.MAX_VALUE; 224 225 for (int i = 0; i < childCount; i++) { 226 final View child = layoutManager.getChildAt(i); 227 int childStart = helper.getDecoratedStart(child); 228 229 /** if child is more to start than previous closest, set it as closest **/ 230 if (childStart < startest) { 231 startest = childStart; 232 closestChild = child; 233 } 234 } 235 return closestChild; 236 } 237 238 @NonNull 239 private OrientationHelper getVerticalHelper(@NonNull RecyclerView.LayoutManager layoutManager) { 240 if (mVerticalHelper == null || mVerticalHelper.mLayoutManager != layoutManager) { 241 mVerticalHelper = OrientationHelper.createVerticalHelper(layoutManager); 242 } 243 return mVerticalHelper; 244 } 245 246 @NonNull 247 private OrientationHelper getHorizontalHelper( 248 @NonNull RecyclerView.LayoutManager layoutManager) { 249 if (mHorizontalHelper == null || mHorizontalHelper.mLayoutManager != layoutManager) { 250 mHorizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager); 251 } 252 return mHorizontalHelper; 253 } 254 } 255