教你控制 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 有幫助。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM