Home | History | Annotate | Download | only in widget
      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
     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
     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
     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