/* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.support.v7.widget; import android.graphics.PointF; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.DisplayMetrics; import android.view.View; /** * Implementation of the {@link SnapHelper} supporting pager style snapping in either vertical or * horizontal orientation. * * <p> * * PagerSnapHelper can help achieve a similar behavior to {@link android.support.v4.view.ViewPager}. * Set both {@link RecyclerView} and the items of the * {@link android.support.v7.widget.RecyclerView.Adapter} to have * {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT} height and width and then attach * PagerSnapHelper to the {@link RecyclerView} using {@link #attachToRecyclerView(RecyclerView)}. */ public class PagerSnapHelper extends SnapHelper { private static final int MAX_SCROLL_ON_FLING_DURATION = 100; // ms // Orientation helpers are lazily created per LayoutManager. @Nullable private OrientationHelper mVerticalHelper; @Nullable private OrientationHelper mHorizontalHelper; @Nullable @Override public int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView) { int[] out = new int[2]; if (layoutManager.canScrollHorizontally()) { out[0] = distanceToCenter(layoutManager, targetView, getHorizontalHelper(layoutManager)); } else { out[0] = 0; } if (layoutManager.canScrollVertically()) { out[1] = distanceToCenter(layoutManager, targetView, getVerticalHelper(layoutManager)); } else { out[1] = 0; } return out; } @Nullable @Override public View findSnapView(RecyclerView.LayoutManager layoutManager) { if (layoutManager.canScrollVertically()) { return findCenterView(layoutManager, getVerticalHelper(layoutManager)); } else if (layoutManager.canScrollHorizontally()) { return findCenterView(layoutManager, getHorizontalHelper(layoutManager)); } return null; } @Override public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX, int velocityY) { final int itemCount = layoutManager.getItemCount(); if (itemCount == 0) { return RecyclerView.NO_POSITION; } View mStartMostChildView = null; if (layoutManager.canScrollVertically()) { mStartMostChildView = findStartView(layoutManager, getVerticalHelper(layoutManager)); } else if (layoutManager.canScrollHorizontally()) { mStartMostChildView = findStartView(layoutManager, getHorizontalHelper(layoutManager)); } if (mStartMostChildView == null) { return RecyclerView.NO_POSITION; } final int centerPosition = layoutManager.getPosition(mStartMostChildView); if (centerPosition == RecyclerView.NO_POSITION) { return RecyclerView.NO_POSITION; } final boolean forwardDirection; if (layoutManager.canScrollHorizontally()) { forwardDirection = velocityX > 0; } else { forwardDirection = velocityY > 0; } boolean reverseLayout = false; if ((layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) { RecyclerView.SmoothScroller.ScrollVectorProvider vectorProvider = (RecyclerView.SmoothScroller.ScrollVectorProvider) layoutManager; PointF vectorForEnd = vectorProvider.computeScrollVectorForPosition(itemCount - 1); if (vectorForEnd != null) { reverseLayout = vectorForEnd.x < 0 || vectorForEnd.y < 0; } } return reverseLayout ? (forwardDirection ? centerPosition - 1 : centerPosition) : (forwardDirection ? centerPosition + 1 : centerPosition); } @Override protected LinearSmoothScroller createSnapScroller(RecyclerView.LayoutManager layoutManager) { if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) { return null; } return new LinearSmoothScroller(mRecyclerView.getContext()) { @Override protected void onTargetFound(View targetView, RecyclerView.State state, Action action) { int[] snapDistances = calculateDistanceToFinalSnap(mRecyclerView.getLayoutManager(), targetView); final int dx = snapDistances[0]; final int dy = snapDistances[1]; final int time = calculateTimeForDeceleration(Math.max(Math.abs(dx), Math.abs(dy))); if (time > 0) { action.update(dx, dy, time, mDecelerateInterpolator); } } @Override protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) { return MILLISECONDS_PER_INCH / displayMetrics.densityDpi; } @Override protected int calculateTimeForScrolling(int dx) { return Math.min(MAX_SCROLL_ON_FLING_DURATION, super.calculateTimeForScrolling(dx)); } }; } private int distanceToCenter(@NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView, OrientationHelper helper) { final int childCenter = helper.getDecoratedStart(targetView) + (helper.getDecoratedMeasurement(targetView) / 2); final int containerCenter; if (layoutManager.getClipToPadding()) { containerCenter = helper.getStartAfterPadding() + helper.getTotalSpace() / 2; } else { containerCenter = helper.getEnd() / 2; } return childCenter - containerCenter; } /** * Return the child view that is currently closest to the center of this parent. * * @param layoutManager The {@link RecyclerView.LayoutManager} associated with the attached * {@link RecyclerView}. * @param helper The relevant {@link OrientationHelper} for the attached {@link RecyclerView}. * * @return the child view that is currently closest to the center of this parent. */ @Nullable private View findCenterView(RecyclerView.LayoutManager layoutManager, OrientationHelper helper) { int childCount = layoutManager.getChildCount(); if (childCount == 0) { return null; } View closestChild = null; final int center; if (layoutManager.getClipToPadding()) { center = helper.getStartAfterPadding() + helper.getTotalSpace() / 2; } else { center = helper.getEnd() / 2; } int absClosest = Integer.MAX_VALUE; for (int i = 0; i < childCount; i++) { final View child = layoutManager.getChildAt(i); int childCenter = helper.getDecoratedStart(child) + (helper.getDecoratedMeasurement(child) / 2); int absDistance = Math.abs(childCenter - center); /** if child center is closer than previous closest, set it as closest **/ if (absDistance < absClosest) { absClosest = absDistance; closestChild = child; } } return closestChild; } /** * Return the child view that is currently closest to the start of this parent. * * @param layoutManager The {@link RecyclerView.LayoutManager} associated with the attached * {@link RecyclerView}. * @param helper The relevant {@link OrientationHelper} for the attached {@link RecyclerView}. * * @return the child view that is currently closest to the start of this parent. */ @Nullable private View findStartView(RecyclerView.LayoutManager layoutManager, OrientationHelper helper) { int childCount = layoutManager.getChildCount(); if (childCount == 0) { return null; } View closestChild = null; int startest = Integer.MAX_VALUE; for (int i = 0; i < childCount; i++) { final View child = layoutManager.getChildAt(i); int childStart = helper.getDecoratedStart(child); /** if child is more to start than previous closest, set it as closest **/ if (childStart < startest) { startest = childStart; closestChild = child; } } return closestChild; } @NonNull private OrientationHelper getVerticalHelper(@NonNull RecyclerView.LayoutManager layoutManager) { if (mVerticalHelper == null || mVerticalHelper.mLayoutManager != layoutManager) { mVerticalHelper = OrientationHelper.createVerticalHelper(layoutManager); } return mVerticalHelper; } @NonNull private OrientationHelper getHorizontalHelper( @NonNull RecyclerView.LayoutManager layoutManager) { if (mHorizontalHelper == null || mHorizontalHelper.mLayoutManager != layoutManager) { mHorizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager); } return mHorizontalHelper; } }