Custom RecyclerView supports fast scrolling

Problem Description:

RecyclerView's built-in fast scrolling cannot control the unique length of the scroll bar, which means that as the number of items increases, the length of the scroll bar will become smaller and smaller.

Solve the problem:

By customizing RecyclerView, the length of the scroll bar will not change due to the increase of items.

Implementation:

package com.emsm.app.widget;

import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.view.MotionEvent;

import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;

import com.emsm.app.util.LogHelps;

/**
 * @Author emsm
 * @Time 2022/9/5 19:08
 * @Description 参考;RecyclerView中的initFastScroller实现
 * <p>
 * 专注美妆(香水口红护肤)批发代发-供淘宝/天猫/京东/微商/代购/闲鱼等
 * 主做欧美大牌:迪奥/阿玛尼/祖马龙/香奈儿/古驰/TF/MAC/圣罗兰等等
 * 只做高品质产品!(送朋友亲人客户公司活动以及自用或泡妞等等)
 * +V:em-smart-99999
 */
public class FastScrollerRecyclerView extends RecyclerView {
    private FastScroller mFastScroller;

    public FastScrollerRecyclerView(Context context) {
        super(context);
    }

    public FastScrollerRecyclerView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs);
    }

    public FastScrollerRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs);
    }

    private void init(Context context, AttributeSet attrs) {
        mFastScroller = new FastScroller(context, attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int mMeasureWidth = MeasureSpec.getSize(widthMeasureSpec);
        int mMeasureHeight = MeasureSpec.getSize(heightMeasureSpec);
        setMeasuredDimension(mMeasureWidth, mMeasureHeight);
        LogHelps.w("setMeasuredDimension ");
        if (mFastScroller != null) {
            mFastScroller.onMeasure(mMeasureWidth, mMeasureHeight);
        }
    }

    private class FastScroller extends RecyclerView.ItemDecoration implements RecyclerView.OnItemTouchListener {
        private ScrollerDraw mScrollerDraw;
        private ScrollerEvent mScrollerEvent;

        FastScroller(Context context, AttributeSet attrs) {
            init(context, attrs);
        }

        private void init(Context context, AttributeSet attrs) {
            mScrollerDraw = new ScrollerDraw();
            mScrollerDraw.init(context, attrs);
            mScrollerDraw.setView(FastScrollerRecyclerView.this);
            if (isEnabled()) {
                addItemDecoration(this);
                addOnItemTouchListener(this);

                mScrollerEvent = new ScrollerEvent();
                mScrollerEvent.attachRecyclerView(FastScrollerRecyclerView.this, ratio -> {
                    setRatio(ratio);
                });
            }
        }

        private void onMeasure(int measureWidth, int measureHeight) {
            if (isEnabled()) {
                mScrollerDraw.onMeasure(measureWidth, measureHeight, measureWidth);
            }
        }

        private boolean isEnabled() {
            return (mScrollerDraw != null && mScrollerDraw.isEnabled());
        }

        @Override
        public void onDrawOver(@NonNull Canvas canvas, @NonNull RecyclerView parent, @NonNull State state) {
            super.onDrawOver(canvas, parent, state);
            try {
                if (!ScrollerEvent.isRecyclerScrollable(FastScrollerRecyclerView.this)) {
                    LogHelps.w("item数量不足一屏 不支持展示滚动条");
                    return;
                }

                if (!isEnabled()) {
                    return;
                }

                if (!mScrollerDraw.isAlwaysShow()) {
                    mScrollerDraw.mH.removeMessages(1);
                    if (mScrollerDraw.getAnimatorWhat() == 1) {
                        mScrollerDraw.mH.sendEmptyMessage(1);
                        mScrollerDraw.setAnimatorWhat(2);
                    }
                }

                mScrollerDraw.onDraw(canvas);

                if (!mScrollerDraw.isAlwaysShow()) {
                    mScrollerDraw.mH.removeMessages(2);
                    if (mScrollerDraw.getAnimatorWhat() == 2) {
                        mScrollerDraw.mH.sendEmptyMessageDelayed(2, 1000);
                    }
                }
            } catch (Exception e) {
                LogHelps.e("Exception-IllegalArgumentException :" + e.getMessage());
            }
        }

        @Override
        public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent ev) {
            // 判断是不是在滚动条范围之内 在就拦截item滚动
            boolean insideVerticalThumb = isPointInsideVerticalThumb(ev.getX(), ev.getY());
            if (ev.getAction() == MotionEvent.ACTION_DOWN && (insideVerticalThumb)) {
                return true;
            }

            return false;
        }

        private boolean isPointInsideVerticalThumb(float x, float y) {
            if (!isEnabled()) {
                return false;
            }

            // SeekbarRecyclerViewCopy.this.getPaddingRight() 获取的值相当于布局文件中 android:paddingRight="30dp"
            // LogHelps.i(SeekbarRecyclerViewCopy.this.getPaddingRight() + " |getPaddingRight " + SizeUtil.px2dip(mContext, 30));
            return x > mScrollerDraw.getThumbBitmapC() - FastScrollerRecyclerView.this.getPaddingRight();
        }

        @Override
        public void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent event) {
            if (mScrollerEvent != null && isEnabled()) {
                mScrollerEvent.onTouchEvent(event, getHeight());
            }
        }

        @Override
        public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {

        }

        public void setRatio(float ratio) {
            if (isEnabled()) {
                mScrollerDraw.setAnimatorWhat(1);
                mScrollerDraw.setRatio(ratio);
                postInvalidate();
            }
        }
    }
}
package com.emsm.app.widget;

import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.drawable.BitmapDrawable;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.AttributeSet;
import android.view.View;

import androidx.annotation.NonNull;

import com.emsm.app.R;
import com.emsm.app.util.BitmapUtils;
import com.emsm.app.util.LogHelps;

/**
 * @Author chentao 0000668668
 * @Time 2023/2/10
 * @Description
 *  * 专注美妆(香水口红护肤)批发代发-供淘宝/天猫/京东/微商/代购/闲鱼等
 *  * 主做欧美大牌:迪奥/阿玛尼/祖马龙/香奈儿/古驰/TF/MAC/圣罗兰等等
 *  * 只做高品质产品!(送朋友亲人客户公司活动以及自用或泡妞等等)
 *  * +V:em-smart-99999
 */
public class ScrollerDraw {
    private Paint mPaint;
    private Matrix matrix;

    private Bitmap mBgBitmap;
    private Bitmap mPgBitmap;
    private Bitmap mThumbBitmap;

    private boolean mEnabled = true;

    // true  说明 一直显示 false 说明 滑动显示不滑动隐藏
    private boolean mAlwaysShow = false;

    // 滚动的比例值 0-1
    private float mRatio = 0;

    private int mDrawHeight;
    private int mThumbBitmapW, mThumbBitmapH, mThumbBitmapC;
    private int mBgBitmapW, mBgBitmapH, mBgBitmapC;
    private int mPgBitmapW, mPgBitmapH, mPgBitmapC;

    private final ValueAnimator mShowHideAnimator = ValueAnimator.ofFloat(0, 1);

    public boolean isAlwaysShow() {
        return mAlwaysShow;
    }

    public boolean isEnabled() {
        return mEnabled;
    }

    public void setRatio(float ratio) {
        this.mRatio = ratio;
    }

    public void init(Context context, AttributeSet attrs) {
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.ScrollBar);

        mEnabled = array.getBoolean(R.styleable.ScrollBar_ScrollBarEnabled, true);
        if (!mEnabled) {
            LogHelps.w(" init-快速滚动设置为不可用!!!");
            array.recycle();
            return;
        }

        mAlwaysShow = array.getBoolean(R.styleable.ScrollBar_ScrollBarAlwaysShow, false);

        BitmapDrawable drawableBg = (BitmapDrawable) array.getDrawable(R.styleable.ScrollBar_ScrollBarBackground);
        if (drawableBg != null) {
            mBgBitmap = drawableBg.getBitmap();
        }

        BitmapDrawable drawablePr = (BitmapDrawable) array.getDrawable(R.styleable.ScrollBar_ScrollBarProgress);
        if (drawablePr != null) {
            mPgBitmap = drawablePr.getBitmap();
        }

        BitmapDrawable drawableTb = (BitmapDrawable) array.getDrawable(R.styleable.ScrollBar_ScrollBarThumb);
        if (drawableTb != null) {
            mThumbBitmap = drawableTb.getBitmap();
        }

        mRatio = array.getInt(R.styleable.ScrollBar_ScrollBarCurrentProgress, 0);

        array.recycle();

        mPaint = new Paint();
        mPaint.setAntiAlias(true);

        if (!mAlwaysShow) {
            mShowHideAnimator.addUpdateListener(new AnimatorUpdater());
            mH.sendEmptyMessage(1);
        }
    }

    private int mAnimatorWhat = 2;
    private static final int SCROLLBAR_FULL_OPAQUE = 255;

    private View mView;

    public void setView(View view) {
        this.mView = view;
    }

    public int getAnimatorWhat() {
        return mAnimatorWhat;
    }

    public void setAnimatorWhat(int mAnimatorWhat) {
        this.mAnimatorWhat = mAnimatorWhat;
    }

    public final Handler mH = new Handler(Looper.getMainLooper()) {
        @Override
        public void handleMessage(@NonNull Message msg) {
            super.handleMessage(msg);
            switch (msg.what) {
                case 1:  // 显示
                    LogHelps.i(" handleMessage show");
                    mPaint.setAlpha(225);
                    if (false) {
                        mShowHideAnimator.setFloatValues((float) mShowHideAnimator.getAnimatedValue(), 1);
                        mShowHideAnimator.setDuration(100);
                        //  mShowHideAnimator.setStartDelay(0);
                        mShowHideAnimator.start();
                    }
                    break;
                case 2: // 隐藏
                    LogHelps.i(" handleMessage hide");
                    if (mShowHideAnimator.isRunning()) {
                        mShowHideAnimator.cancel();
                    }
                    mShowHideAnimator.setFloatValues((float) mShowHideAnimator.getAnimatedValue(), 0);
                    mShowHideAnimator.setDuration(100);
                    mShowHideAnimator.start();
                    break;
            }
        }
    };

    private class AnimatorUpdater implements ValueAnimator.AnimatorUpdateListener {
        AnimatorUpdater() {
        }

        @Override
        public void onAnimationUpdate(ValueAnimator valueAnimator) {
            int alpha = (int) (SCROLLBAR_FULL_OPAQUE * ((float) valueAnimator.getAnimatedValue()));
            // 隐藏之后把数字修改未其他的值 避免再次执行 显示和隐藏的逻辑
            mAnimatorWhat = 3;
            mPaint.setAlpha(alpha);
            if (mView != null) {
                mView.postInvalidate();
            }
        }
    }

    public void onMeasure(int measureWidth, int measureHeight,int left) {
        if (!mEnabled) {
            return;
        }

        if (mBgBitmap != null) {
            mBgBitmap = BitmapUtils.alterBitmapSize(mBgBitmap, mBgBitmap.getWidth(), measureHeight);
        }

        if (mPgBitmap != null) {
            mPgBitmap = BitmapUtils.alterBitmapSize(mPgBitmap, mPgBitmap.getWidth(), measureHeight);
        }

        mThumbBitmapW = mThumbBitmap.getWidth();
        mThumbBitmapH = mThumbBitmap.getHeight();

        mBgBitmapW = mBgBitmap.getWidth();
        mBgBitmapH = mBgBitmap.getHeight();

        mPgBitmapW = mPgBitmap.getWidth();
        mPgBitmapH = mPgBitmap.getHeight();

        if (measureHeight > mPgBitmapH) {
            measureHeight = mPgBitmapH;
        }

        this.mDrawHeight = measureHeight - mThumbBitmapH;
        mThumbBitmapC = left - mThumbBitmapW / 2;
        mBgBitmapC = left - mBgBitmapW / 2;
        mPgBitmapC = left - mPgBitmapW / 2;
    }

    public int getThumbBitmapC() {
        return mThumbBitmapC;
    }

    public void onDraw(Canvas canvas) {
        if (!mEnabled) {
            return;
        }

        try {
            float thumbBitmapTop = 0;
            if (mRatio > 0) {
                thumbBitmapTop = mRatio * mDrawHeight;
            }

            if (mThumbBitmap != null && mThumbBitmapW > 0 && mThumbBitmapH > 0) {
                canvas.drawBitmap(mThumbBitmap, mThumbBitmapC, thumbBitmapTop, mPaint);
            }

            if (mBgBitmap != null && mBgBitmapW > 0 && mBgBitmapH > 0) {
                canvas.drawBitmap(mBgBitmap, mBgBitmapC, thumbBitmapTop + mThumbBitmapH, mPaint);
            }

            if (mPgBitmap != null && mPgBitmapW > 0 && mPgBitmapH > 0 && mDrawHeight > 0 && mRatio > 0) {
                matrix = new Matrix();
                matrix.setScale(1, mRatio);
                Bitmap bitmap = Bitmap.createBitmap(mPgBitmap, 0, 0, mPgBitmapW, mDrawHeight, matrix, true);
                canvas.drawBitmap(bitmap, mPgBitmapC, 0, mPaint);
            }
        } catch (Exception e) {
            LogHelps.e("Exception-IllegalArgumentException :" + e.getMessage());
        }
    }

}
package com.emsm.app.widget;

import android.view.MotionEvent;

import androidx.annotation.NonNull;
import androidx.core.view.ViewCompat;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.StaggeredGridLayoutManager;

import com.emsm.app.util.LogHelps;

/**
 * @Author chentao 0000668668
 * @Time 2023/2/10
 * @Description
 *  * 专注美妆(香水口红护肤)批发代发-供淘宝/天猫/京东/微商/代购/闲鱼等
 *  * 主做欧美大牌:迪奥/阿玛尼/祖马龙/香奈儿/古驰/TF/MAC/圣罗兰等等
 *  * 只做高品质产品!(送朋友亲人客户公司活动以及自用或泡妞等等)
 *  * +V:em-smart-99999
 */
public class ScrollerEvent {
    private RecyclerView mRecyclerView;

    private float mInitialBarHeight;
    private float mLastPressedYAdjustedToInitial;
    private int mLastAppBarLayoutOffset;

    public void attachRecyclerView(RecyclerView recyclerView, CallBack call) {
        this.mRecyclerView = recyclerView;
        this.mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
                super.onScrollStateChanged(recyclerView, newState);
                LogHelps.i("");
            }

            @Override
            public void onScrolled(@NonNull RecyclerView parent, int dx, int dy) {
                super.onScrolled(parent, dx, dy);
                if (call == null) {
                    return;
                }

                // 滚动条拇指的垂直范围
                float extent = parent.computeVerticalScrollExtent();
                //  可滚动的区域大小
                float range = parent.computeVerticalScrollRange();
                // 当前偏移量(当前滚动的距离)
                float offset = parent.computeVerticalScrollOffset();
                // 最大偏移量(最大可滚动的距离)
                float maxOffset = range - extent;
                // 可以滑动时,在绘制
                if (maxOffset > 0) {
                    // float offsetY = ratio * mMeasureHeight;
                    float ratio = offset / maxOffset;
                    LogHelps.i("dx:" + dx +
                            " dy:" + dy +
                            " extent:" + extent +
                            " range:" + range +
                            " offset:" + offset +
                            " maxOffset:" + maxOffset +
                            " ratio:" + ratio);

                    call.onScrolled(ratio);
                }
            }
        });
    }

    public boolean onTouchEvent(MotionEvent event, int viewHeight) {
        if (mRecyclerView == null || event == null) {
            return true;
        }

        if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
            mRecyclerView.stopScroll();

            int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
            nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;

            mRecyclerView.startNestedScroll(nestedScrollAxis);

            mInitialBarHeight = viewHeight;
            mLastPressedYAdjustedToInitial = event.getY() + 0;
        } else if (event.getActionMasked() == MotionEvent.ACTION_MOVE) {
            float newHandlePressedY = event.getY() + 0;
            int barHeight = viewHeight;
            float newHandlePressedYAdjustedToInitial = newHandlePressedY + (mInitialBarHeight - barHeight);
            float deltaPressedYFromLastAdjustedToInitial = newHandlePressedYAdjustedToInitial - mLastPressedYAdjustedToInitial;

            int dY = (int) ((deltaPressedYFromLastAdjustedToInitial / mInitialBarHeight) * (mRecyclerView.computeVerticalScrollRange() + 0));
            updateRvScroll(dY + mLastAppBarLayoutOffset);

            mLastPressedYAdjustedToInitial = newHandlePressedYAdjustedToInitial;
        } else if (event.getActionMasked() == MotionEvent.ACTION_UP) {
            mLastPressedYAdjustedToInitial = -1;
            mRecyclerView.stopNestedScroll();
        }
        return true;
    }

    public void updateRvScroll(int dY) {
        if (mRecyclerView == null) {
            return;
        }
        try {
            mRecyclerView.scrollBy(0, dY);
        } catch (Exception t) {
            t.printStackTrace();
        }
    }

    interface CallBack {
        // 滚动的比例值 0-1
        void onScrolled(float ratio);
    }

    /**
     * 判断是否可以滚动
     * @param recyclerView
     * @return
     */
    public static boolean isRecyclerScrollable(RecyclerView recyclerView) {
        if (recyclerView == null) {
            return false;
        }

        float range = recyclerView.computeVerticalScrollRange();
        float height = recyclerView.getHeight();
        // LogHelps.i("recyclerView的滚动范围 " + range + " | RecyclerView的高度 " + height);
        // 滚动范围大于RecyclerView的高度 说明是可以滚动的
        if (true) {
            return range > height;
        }

        boolean h = false;
        if (recyclerView.getLayoutManager() instanceof LinearLayoutManager) {
            LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
            RecyclerView.Adapter adapter = recyclerView.getAdapter();
            if (layoutManager == null || adapter == null) {
                h = false;
            } else {
                h = layoutManager.findLastCompletelyVisibleItemPosition() < adapter.getItemCount() - 1;
            }
        } else if (recyclerView.getLayoutManager() instanceof GridLayoutManager) {
            GridLayoutManager layoutManager = (GridLayoutManager) recyclerView.getLayoutManager();
            RecyclerView.Adapter adapter = recyclerView.getAdapter();
            if (layoutManager == null || adapter == null) {
                h = false;
            } else {
                h = layoutManager.findLastCompletelyVisibleItemPosition() < adapter.getItemCount() - 1;
            }
        } else if (recyclerView.getLayoutManager() instanceof StaggeredGridLayoutManager) {
            StaggeredGridLayoutManager layoutManager = (StaggeredGridLayoutManager) recyclerView.getLayoutManager();
            RecyclerView.Adapter adapter = recyclerView.getAdapter();
            if (layoutManager == null || adapter == null) {
                h = false;
            } else {
                h = layoutManager.findLastCompletelyVisibleItemPositions(null)[(layoutManager.getSpanCount() - 1)] < adapter.getItemCount() - 1;
            }
        }

        return h;
    }

}
    <com.emsm.app.widget.FastScrollerRecyclerView
        android:id="@+id/recycler"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_marginRight="100dp"
        android:background="@color/E3E3E3E3"
        android:paddingRight="30dp"
        app:ScrollBarAlwaysShow="true"
        app:ScrollBarBackground="@mipmap/ad3_bt_contacts_slider_n"
        app:ScrollBarBottom="20dp"
        app:ScrollBarCurrentProgress="0"
        app:ScrollBarEnabled="true"
        app:ScrollBarProgress="@mipmap/ad3_bt_contacts_slider_n"

        app:ScrollBarThumb="@mipmap/ad3_bt_contacts_slider_d"
        app:ScrollBarTop="20dp"

        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

Specific effect:

Attached: pictures used

Guess you like

Origin blog.csdn.net/CHNE_TAO_EMSM/article/details/128853135