问题描述:
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" />
具体效果:
附带:使用到的图片