• 教你控制 RecyclerView 滑动的节奏


    最近,PM升级改版落地页,其中有一个很奇怪的交互需求,需求是这样的:

      用户在该页面可以上下无限滑动,但是,在上拉滑动过程中,当内容切换为另一个内容的时候,新的内容先吸顶,然后停止滑动,让用户知道他已经滑到一个新的内容区了。同一个内容里面,没有该约束。下拉滑动过程也没有这种约束。

      或者用户没有滑动,但是点击到了新的内容区,也需要将新的内容缓慢吸顶,方便用户阅读。

    作为RD的我,表示非常蛋疼啊,既然需求是这样的,作为万能的RD的我,当然得想办法去解决呢。

    首先,选择 RecyclerView 作为滑动的容器,难点就是怎么在滑动过程中,将新的内容页吸顶,停止滑动。对于 RecyclerView, 当用户滑动后,最终通过 fling 方法来实现惯性滑动的,因此,必须拦截该方法,做一些特有的操作。

    试了试 RecyclerView 自身的方法和管理器,都不能实现缓慢吸顶,看来只能重写其中的一些方法了。

    public class MyLinearLayoutManger extends LinearLayoutManager {
        private float MILLISECONDS_PER_INCH = 0.03f;
        private Context contxt;
    
        public MyLinearLayoutManger(Context context) {
            super(context);
            this.contxt = context;
           // setSpeedSlow();
        }
    
        @Override
        public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {
            LinearSmoothScroller linearSmoothScroller =
                    new LinearSmoothScroller(recyclerView.getContext()) {
                        @Override
                        public PointF computeScrollVectorForPosition(int targetPosition) {
                            return MyLinearLayoutManger.this.computeScrollVectorForPosition(targetPosition);
                        }
    
                        //This returns the milliseconds it takes to
                        //scroll one pixel.
                        @Override
                        protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
                            return MILLISECONDS_PER_INCH / displayMetrics.density;
                            //返回滑动一个pixel需要多少毫秒
                        }
    
                        @Override
                        public int calculateDtToFit(int viewStart, int viewEnd, int boxStart, int boxEnd, int snapPreference) {
                            return boxStart - viewStart;
                        }
                    };
            linearSmoothScroller.setTargetPosition(position);
            startSmoothScroll(linearSmoothScroller);
    
        }
    }

    我们来看 LinearSmoothScroller 中的一段源码:

    /**
         * Helper method for {@link #calculateDxToMakeVisible(android.view.View, int)} and
         * {@link #calculateDyToMakeVisible(android.view.View, int)}
         */
        public int calculateDtToFit(int viewStart, int viewEnd, int boxStart, int boxEnd, int
                snapPreference) {
            switch (snapPreference) {
                case SNAP_TO_START:
                    return boxStart - viewStart;
                case SNAP_TO_END:
                    return boxEnd - viewEnd;
                case SNAP_TO_ANY:
                    final int dtStart = boxStart - viewStart;
                    if (dtStart > 0) {
                        return dtStart;
                    }
                    final int dtEnd = boxEnd - viewEnd;
                    if (dtEnd < 0) {
                        return dtEnd;
                    }
                    break;
                default:
                    throw new IllegalArgumentException("snap preference should be one of the"
                            + " constants defined in SmoothScroller, starting with SNAP_");
            }
            return 0;
        }

    可以看到,SNAP_TO_START 就是上边或者左边吸顶的意思。这样只要重写 calculateDtToFit 方法就实现了缓慢吸顶。

      /**
         * Align child view's left or top with parent view's left or top
         *
         * @see #calculateDtToFit(int, int, int, int, int)
         * @see #calculateDxToMakeVisible(android.view.View, int)
         * @see #calculateDyToMakeVisible(android.view.View, int)
         */
        public static final int SNAP_TO_START = -1;

    那什么时候调用呢?一个是点击新的内容区时候调用,另一个是再滑动过程中判断是否要切换内容区了。这时候就要重写 

    package com.sjq.recycletest;
    
    import android.content.Context;
    import android.hardware.SensorManager;
    import android.support.annotation.Nullable;
    import android.support.v7.widget.LinearLayoutManager;
    import android.support.v7.widget.RecyclerView;
    import android.util.AttributeSet;
    import android.util.Log;
    import android.view.MotionEvent;
    import android.view.View;
    import android.view.ViewConfiguration;
    
    /**
     * Created by shenjiaqi on 2018/7/18.
     */
    
    public class MyRecyclerView extends RecyclerView {
    
        private static float DECELERATION_RATE = (float) (Math.log(0.78) / Math.log(0.9));
        private static final float INFLEXION = 0.35f; // Tension lines cross at (INFLEXION, 1)
        private float mFlingFriction = ViewConfiguration.getScrollFriction();
        private float mPhysicalCoeff;
        private static final String TAG = "MyRecyclerView";
        private final int height;
    
        public MyRecyclerView(Context context) {
            this(context, null);
        }
    
        public MyRecyclerView(Context context, @Nullable AttributeSet attrs) {
            this(context, attrs, 0);
        }
    
        public MyRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
            super(context, attrs, defStyle);
    
            final float ppi = context.getResources().getDisplayMetrics().density * 160.0f;
            mPhysicalCoeff = SensorManager.GRAVITY_EARTH // g (m/s^2)
                    * 39.37f // inch/meter
                    * ppi
                    * 0.84f; // look and feel tuning
    
            height = context.getResources().getDisplayMetrics().heightPixels;
        }
    
        public double getSplineDeceleration(int velocity) {
            return Math.log(INFLEXION * Math.abs(velocity) / (mFlingFriction * mPhysicalCoeff));
        }
      // 惯性滑动的距离
        public double getSplineFlingDistance(int velocity) {
            final double l = getSplineDeceleration(velocity);
            final double decelMinusOne = DECELERATION_RATE - 1.0;
            return mFlingFriction * mPhysicalCoeff * Math.exp(DECELERATION_RATE / decelMinusOne * l);
        }
    
        /* Returns the duration, expressed in milliseconds */
        public int getSplineFlingDuration(int velocity) {
            final double l = getSplineDeceleration(velocity);
            final double decelMinusOne = DECELERATION_RATE - 1.0;
            return (int) (1000.0 * Math.exp(l / decelMinusOne));
        }
    
        @Override
        public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
            super.onNestedPreScroll(target, dx, dy, consumed);
        }
    
        @Override
        public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
            return super.onNestedPreFling(target, velocityX, velocityY);
        }
    
        @Override
        public boolean fling(int velocityX, int velocityY) {
            if (velocityY > 0) {
                if (getLayoutManager() != null
                        && getLayoutManager() instanceof LinearLayoutManager) {
                    LinearLayoutManager manager = (LinearLayoutManager) getLayoutManager();
                    final int firstPosition = manager.findFirstVisibleItemPosition();
                    final int lastPosition = manager.findLastVisibleItemPosition();
              // 假设一个item高度为 500, 通过惯性滑动距离和高度可以计算出来会经过多少个item
    int position = (int) (firstPosition + getSplineFlingDistance((int) velocityY) / 500);
              // 以8个item为一个内容区
    int s1 = firstPosition / 8; int s = lastPosition > position ? lastPosition / 8 : position / 8; if (s > s1) { s = s1 + 1; } int pos = s * 8; int top = height; if (s > s1 && lastPosition >= pos && pos > firstPosition) { top = getChildAt(pos - firstPosition).getTop(); } if (s > 0 && s > s1) { smoothScrollToPosition(pos); return true; } } } return super.fling(velocityX, velocityY); } public int getDisplyHeight() { return height; } }

    但是大家会发现,如果 item 高度值估计过小,会导致,一滑动就会立马切换到新的内容区,体验还是不好。

    眼尖的人一定会发现,我在前面 MyLinearLayoutManger 构造函数中注释掉了一个方法。这个方法就是用来改善滑动效果的。

     public void setSpeedSlow() {
            //自己在这里用density去乘,希望不同分辨率设备上滑动速度相同
            //0.3f是自己估摸的一个值,可以根据不同需求自己修改
            MILLISECONDS_PER_INCH = contxt.getResources().getDisplayMetrics().density * 0.03f;
        }

    调用该方法后,大家会发现效果好多了,但是其实默认值  MILLISECONDS_PER_INCH 是 25f;

    你会发现效果似乎更好了。

    希望本文对大家进一步了解 RecyclerView 有帮助。

  • 相关阅读:
    Android AdapterView View的复用机制 分析
    go12---interface
    go11---方法method
    go10---struct
    go09---defer
    go8---函数function
    go7---map
    go6---slice切片
    go5--数组
    go4--break,continue + 标签
  • 原文地址:https://www.cnblogs.com/huansky/p/9382689.html
Copyright © 2020-2023  润新知