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