自定义RecyclerView支持快速滚动

问题描述:

RecyclerView自带快速滚动无法控制滚动条的长度唯一,也就是说随着item的增多,滚动条的长度会越变越小。

解决问题:

通过自定义RecyclerView来实现滚动条的长度不会因为item的增多而发生长度变化。

具体实现:

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" />

具体效果:

附带:使用到的图片

猜你喜欢

转载自blog.csdn.net/CHNE_TAO_EMSM/article/details/128853135
今日推荐