| /* |
| * 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; |
| } |
| } |