1.核心类结构速览
Digital_Rolling_RecyclerView
初始化
├── setParent(FrameLayout) // 父容器
├── setContext(Context) // MainActivity.this
├── setAdapterCreator(AdapterCreator) // Adapter创建器
├── setTextSize(normal, selected) // 文字大小配置
├── setVisibleItemCount(count) // 可见item数量
├── setPlaceholderCount(head, tail) // 首尾占位数量
├── setTargetPosition(pos) // 初始选中位置
├── setClickToJump(boolean) // 是否开启点击跳转
├── setOnScrollListener(listener) // 滚动与点击监听(用于更新ui)
├── apply() // 执行初始化,返回RecyclerView
外部调用
├── scrollToPosition(pos, isChengXuGunDong) // 程序滚动到指定位置
工具类内部作用域的方法
└── updatePickerEffects() // 更新缩放与透明度(内部调用)
2.使用示例
java
mDigitalRollingRecyclerView = new Digital_Rolling_RecyclerView().setParent(f)
.setContext(mContext)
// 语法糖,等价于如下代码,相当于传入了一个回调。
/*
new AdapterCreator() {
@Override
public RecyclerView.Adapter create(int itemHeight) {
return new DigitalRollingRecyclerViewAdapter(itemHeight);
}
}
*/
.setAdapterCreator(DigitalRollingRecyclerViewAdapter::new)
.setClickToJump(true)
.setTextSize(TEXT_SIZE_NORMAL, TEXT_SIZE_SELECTED)
.setVisibleItemCount(3)
.setPlaceholderCount(1, 1)
.setTargetPosition(8)// temperature_List索引8对应"24"
.setOnScrollListener(new Digital_Rolling_RecyclerView.OnScrollListener() {
@Override
public void onScroll(boolean isScrolling, int position, boolean isChengXuGunDong) {
// todo:这里是滚动事件相关的操作被触发时,更新ui的相关操作。
}
@Override
public void onItemClick(int position) {
// todo:这里是textView被点击后,更新ui的相关操作。
}
});
// 使用工具类完成Picker全部初始化:创建RV、计算高度、创建Adapter、配置吸附与滚动效果
mRecyclerView = mDigitalRollingRecyclerView.apply();
3.注意事项
3.1 自定义RecyclerView适配器(需传入itemHeight -> 控件高度)
由于需要传入itemHeight,所以工具类中才需要传入DigitalRollingRecyclerViewAdapter::new,而不是 new DigitalRollingRecyclerViewAdapter。 这俩的区别是,new出来的对象已经被创建在了栈中,而DigitalRollingRecyclerViewAdapter::new只是一个回调,真正创建适配的地方在工具类-apply()中的
RecyclerView.Adapter adapter = adapterCreator.create(mItemHeight);
如上代码中。
当然你也可以自己魔改一下,在外面先创建好RecyclerView,然后把该view添加到FrameLayout中。此时你只需要传入itemHeight就好了。
但工具类秉承着用户只需要传入所有的数据,逻辑部分工具类自己内部实现,用户直接拿成品的原则,最好还是不要这么做。
java
private class DigitalRollingRecyclerViewAdapter extends RecyclerView.Adapter<DigitalRollingRecyclerViewAdapter.ViewHolder>{
private int itemHeight;
DigitalRollingRecyclerViewAdapter(int itemHeight) {
this.itemHeight = itemHeight;
}
@NonNull
@Override
public DigitalRollingRecyclerViewAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
TextView textView = new TextView(getContext());
textView.setLayoutParams(new RecyclerView.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
itemHeight
));
textView.setGravity(Gravity.START | Gravity.CENTER_VERTICAL); // 水平居左
textView.setTextColor(getContext().getColor(R.color.TitleTextColor));
textView.setIncludeFontPadding(false);
return new ViewHolder(textView);
}
@Override
public void onBindViewHolder(@NonNull DigitalRollingRecyclerViewAdapter.ViewHolder holder, int position) {
// 首尾为占位item,不显示内容
if (position == 0 || position == temperature_List.length + 1) {
holder.textView.setText("");
holder.textView.setAlpha(0f);
} else {
holder.textView.setText(temperature_List[position - 1]);
holder.textView.setAlpha(1f);
// 默认小文字,由updatePickerEffects根据滚动位置实时调整
holder.textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, TEXT_SIZE_NORMAL);
}
// 调试背景色,确认item占据区域,开发完成后可删除
// holder.textView.setBackground(AppApplication.getContext().getDrawable(R.color.BarColor1));
}
@Override
public int getItemCount() {
// 真实数据 + 首尾各一个占位,确保Hi和Lo能滚到视觉中心
return temperature_List.length + 2;
}
class ViewHolder extends RecyclerView.ViewHolder{
TextView textView;
ViewHolder(TextView textView){
super(textView);
this.textView = textView;
}
}
}
4.工具类完整代码
java
package com.sth.Uitility;
import android.content.Context;
import android.util.Log;
import android.util.TypedValue;
import android.view.MotionEvent; // 新增:点击监听需要
import android.view.View;
import android.widget.FrameLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.LinearSmoothScroller;
import androidx.recyclerview.widget.LinearSnapHelper;
import androidx.recyclerview.widget.RecyclerView;
/**
* @File Name: Digital_Rolling_RecyclerView.java
* @Date: March 5, 2026
* @Version: 1.0
* @Author: 施棠海
* @描述: 数字滚动选择器效果配置器。
* @: 负责:在父容器中自动创建RecyclerView、计算item高度、配置吸附回弹与滚动视觉效果
* @: 业务方需提供AdapterCreator以注入业务数据与ViewHolder
*/
public class Digital_Rolling_RecyclerView {
private static final String tag = "Digital_Rolling_RecyclerView";
private FrameLayout parent; // 父容器
private Context context; // MainActivity.this Context
private AdapterCreator adapterCreator; // Adapter创建器,接收itemHeight返回Adapter
private float textSizeNormal = 60f; // 未选中状态文字大小(SP)
private float textSizeSelected = 90f; // 选中状态文字大小(SP)
private int headPlaceholder = 1; // 头部占位item数量
private int tailPlaceholder = 1; // 尾部占位item数量
private int targetPosition = 1; // 初始选中位置(Adapter绝对position)
private int visibleItemCount = 3; // 可见item数量
private int realIndex;
private boolean clickToJump = true; // 点击跳转
private boolean isChengXuGunDong = false; // 该滚动是否是程序滚动
private RecyclerView mRecyclerView; // 内部创建的RecyclerView
private OnScrollListener listener;
// 新增:item高度与SnapHelper实例,供点击跳转时计算精确滚动距离
private int mItemHeight;
private LinearSnapHelper mSnapHelper;
/**
* Adapter创建器接口
* 由业务方实现,根据计算后的item高度创建对应Adapter
*/
public interface AdapterCreator {
RecyclerView.Adapter create(int itemHeight);
}
public interface OnScrollListener {
/**
* 滚动状态监听
* @param isScrolling 滚动中 true/否
*/
void onScroll(boolean isScrolling,int position, boolean isChengXuGunDong);
/**
* item点击监听
* @param position 点击位置
*/
void onItemClick(int position);
}
public Digital_Rolling_RecyclerView setOnScrollListener(OnScrollListener listener) {
this.listener = listener;
return this;
}
/**
* 设置父容器
* @param parent FrameLayout
* @return Digital_Rolling_RecyclerView
*/
public Digital_Rolling_RecyclerView setParent(FrameLayout parent) {
this.parent = parent;
return this;
}
/**
* 设置Context
* @param context Context MainActivity.this
* @return Digital_Rolling_RecyclerView
*/
public Digital_Rolling_RecyclerView setContext(Context context) {
this.context = context;
return this;
}
/**
* 设置Adapter创建器
* @param creator AdapterCreator
* @return Digital_Rolling_RecyclerView
*/
public Digital_Rolling_RecyclerView setAdapterCreator(AdapterCreator creator) {
this.adapterCreator = creator;
return this;
}
/**
* 设置是否开启点击跳转
* @param clickToJump boolean
* @return Digital_Rolling_RecyclerView
*/
public Digital_Rolling_RecyclerView setClickToJump(boolean clickToJump) {
this.clickToJump = clickToJump;
return this;
}
/**
* 设置文字大小
* @param textSizeNormal float 正常状态文字大小
* @param textSizeSelected float 选中状态文字大小
* @return Digital_Rolling_RecyclerView
*/
public Digital_Rolling_RecyclerView setTextSize(float textSizeNormal, float textSizeSelected) {
this.textSizeNormal = textSizeNormal;
this.textSizeSelected = textSizeSelected;
return this;
}
/**
* 设置可见item数量
* @param visibleItemCount int 可见item数量
* @return Digital_Rolling_RecyclerView
*/
public Digital_Rolling_RecyclerView setVisibleItemCount(int visibleItemCount) {
this.visibleItemCount = visibleItemCount;
return this;
}
/**
* 设置头部、尾部占位item数量
* @param head int 头部占位item数量
* @param tail int 尾部占位item数量
* @return Digital_Rolling_RecyclerView
*/
public Digital_Rolling_RecyclerView setPlaceholderCount(int head, int tail) {
this.headPlaceholder = head;
this.tailPlaceholder = tail;
return this;
}
/**
* 设置初始选中位置
* @param targetPosition
* @return Digital_Rolling_RecyclerView
*/
public Digital_Rolling_RecyclerView setTargetPosition(int targetPosition) {
this.targetPosition = targetPosition;
return this;
}
/**
* 执行初始化:创建RV、addView、计算高度、创建Adapter、配置效果
* @return 创建并配置完成的RecyclerView
*/
public RecyclerView apply() {
if (parent == null || context == null || adapterCreator == null) {
Log.e(tag, "parent、context、adapterCreator必须提前设置");
return null;
}
// 创建RecyclerView,高度MATCH_PARENT填满父容器,单列竖向,item间零间距
mRecyclerView = new GridRecyclerFactory(
context,
-1, -1,
0, 0, 0, 0, 0)
.setNumRowsAndDColumns(1, 3)
.recyclerViewinit(true);
parent.addView(mRecyclerView);
// 延迟到布局完成后获取容器实际高度,动态计算item高度(均分3份),再创建Adapter并配置滚动效果
// 必须在post中执行,因为addView后尚未measure,此时getHeight()为0
mRecyclerView.post(() -> {
int containerHeight = mRecyclerView.getHeight();
if (containerHeight <= 0) {
Log.e(tag, "容器高度异常: " + containerHeight);
return;
}
// visibleItemCount个item均分容器高度,无间距,确保中间item中位线与容器中位线重合
mItemHeight = containerHeight / visibleItemCount;
Log.i(tag, "容器高度=" + containerHeight + " item高度=" + mItemHeight);
RecyclerView.Adapter adapter = adapterCreator.create(mItemHeight);
// 吸附到中心:滚动停止后自动将最近item的中位线回弹到容器中心
// 使用LinearSnapHelper而非PagerSnapHelper,前者针对单item中心吸附更精准,后者按整页吸附容易错位
mSnapHelper = new LinearSnapHelper();
mSnapHelper.attachToRecyclerView(mRecyclerView);
// 边界回弹效果(overscroll时发光/拉伸)
mRecyclerView.setOverScrollMode(View.OVER_SCROLL_ALWAYS);
// 绑定Adapter
mRecyclerView.setAdapter(adapter);
// 滚动时动态调整文字大小与透明度
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
updatePickerEffects(recyclerView, adapter.getItemCount());
// 获取当前滚动状态
int state = mRecyclerView.getScrollState();
// 0=SCROLL_STATE_IDLE(停止) 1=DRAGGING(手指拖动) 2=SETTLING(惯性/吸附中)
boolean isScrolling = state != RecyclerView.SCROLL_STATE_IDLE;
// 滚动完全停止
View centerView = mSnapHelper.findSnapView(recyclerView.getLayoutManager());
int position = recyclerView.getChildAdapterPosition(centerView);
// 减去头部占位得到真实数据索引
realIndex = position - headPlaceholder;
listener.onScroll(isScrolling,realIndex,isChengXuGunDong);
}
});
// 滚动到初始位置
mRecyclerView.scrollToPosition(targetPosition);
// scrollToPosition不会触发onScrolled,需手动执行一次视觉效果更新
mRecyclerView.post(() -> updatePickerEffects(mRecyclerView, adapter.getItemCount()));
if (clickToJump) {
// 点击item自动吸附跳转:点击哪个真实数据item,就滚动到视觉中心
mRecyclerView.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() {
private float downX, downY;
private static final float CLICK_THRESHOLD = 10f; // 像素阈值,超过视为滑动
@Override
public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
switch (e.getAction()) {
case MotionEvent.ACTION_DOWN:
downX = e.getX();
downY = e.getY();
break;
case MotionEvent.ACTION_UP:
float dx = Math.abs(e.getX() - downX);
float dy = Math.abs(e.getY() - downY);
// 位移小于阈值判定为点击
if (dx < CLICK_THRESHOLD && dy < CLICK_THRESHOLD) {
View child = rv.findChildViewUnder(e.getX(), e.getY());
if (child != null) {
int targetPos = rv.getChildAdapterPosition(child);
if (targetPos == RecyclerView.NO_POSITION) break;
int total = rv.getAdapter() != null ? rv.getAdapter().getItemCount() : 0;
// 忽略头部/尾部占位item的点击,不执行跳转
if (targetPos < headPlaceholder || targetPos >= total - tailPlaceholder) {
Log.i(tag, "点击占位item,忽略");
break;
}
scrollToPosition(targetPos,false);
listener.onItemClick(realIndex);
}
}
break;
}
return false; // 不拦截,手指拖动/惯性滚动照常
}
@Override
public void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
}
@Override
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
}
});
}
// 滚动结束后获取中心item的position(结合你已有的LinearSnapHelper)
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(@NonNull RecyclerView rv, int newState) {
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
// 获取当前滚动状态
int state = mRecyclerView.getScrollState();
// 0=SCROLL_STATE_IDLE(停止) 1=DRAGGING(手指拖动) 2=SETTLING(惯性/吸附中)
boolean isScrolling = state != RecyclerView.SCROLL_STATE_IDLE;
// 滚动完全停止
View centerView = mSnapHelper.findSnapView(rv.getLayoutManager());
int position = rv.getChildAdapterPosition(centerView);
// 减去头部占位得到真实数据索引
int realIndex = position - headPlaceholder;
listener.onScroll(isScrolling,realIndex,isChengXuGunDong);
isChengXuGunDong = false;
}
}
});
});
return mRecyclerView;
}
/**
* 滚动到指定位置
* @param position 待滚动位置
* @param isChengXuGunDong 是否是程序滚动
*/
public void scrollToPosition(int position,boolean isChengXuGunDong){
this.isChengXuGunDong = isChengXuGunDong;
// 使用自定义LinearSmoothScroller实现精确居中滚动
// 重写calculateDyToMakeVisible,返回将目标View中心与RV中心对齐的像素距离
RecyclerView.LayoutManager lm = mRecyclerView.getLayoutManager();
if (lm instanceof LinearLayoutManager) {
LinearSmoothScroller scroller = new LinearSmoothScroller(mRecyclerView.getContext()) {
@Override
public int calculateDyToMakeVisible(View view, int snapPreference) {
RecyclerView.LayoutManager layoutManager = getLayoutManager();
if (layoutManager == null || !layoutManager.canScrollVertically()) {
return 0;
}
// 获取目标View的上下边界(含装饰边距)
int top = layoutManager.getDecoratedTop(view);
int bottom = layoutManager.getDecoratedBottom(view);
// 获取RecyclerView内容区域的上下边界
int parentTop = layoutManager.getPaddingTop();
int parentBottom = layoutManager.getHeight() - layoutManager.getPaddingBottom();
// 计算中心线
int parentCenter = (parentTop + parentBottom) / 2;
int childCenter = top + (bottom - top) / 2;
// 返回值:正数向下滚,负数向上滚,直到childCenter与parentCenter重合
return parentCenter - childCenter;
}
};
scroller.setTargetPosition(position);
((LinearLayoutManager) lm).startSmoothScroll(scroller);
Log.i(tag, "SmoothScroller居中滚动到position=" + position);
}
}
/**
* 更新Picker视觉效果:中间item变大+不透明,上下item变小+变淡
* 根据每个item中心与RecyclerView中心的距离计算scale与alpha
*/
private void updatePickerEffects(RecyclerView recyclerView, int totalItemCount) {
int centerY = recyclerView.getHeight() / 2; // RecyclerView中心线Y坐标
for (int i = 0; i < recyclerView.getChildCount(); i++) { // 遍历当前可见的所有item
View child = recyclerView.getChildAt(i); // 获取当前item视图
if (child == null) continue; // 防御性校验,避免空指针
int pos = recyclerView.getChildAdapterPosition(child); // 获取该item在Adapter中的绝对位置
// 首尾占位item完全隐藏,不参与视觉效果计算
if (pos < headPlaceholder || pos >= totalItemCount - tailPlaceholder) {
child.setAlpha(0f); // 占位item不可见
continue; // 跳过后续计算
}
int childCenterY = child.getTop() + (child.getHeight() / 2); // 计算item中心点Y坐标
float distance = Math.abs(centerY - childCenterY); // 计算与中心线的绝对距离
float maxDist = child.getHeight(); // 最大参考距离为一个item高度
// 距离比例:0(正中)~ 1(偏离一个item高度)
float ratio = Math.min(1f, distance / maxDist); // 限制ratio上限为1,避免过度衰减
// alpha: 正中1.0,偏离后线性降低到0.2
float alpha = 1.0f - (0.8f * ratio); // 1.0 - 0.8 = 0.2,即最淡时保留20%透明度
child.setAlpha(alpha); // 应用透明度
// 文字大小: 正中textSizeSelected,偏离后线性缩小到textSizeNormal
TextView tv = (TextView) child; // 强转为TextView以修改文字大小
float textSize = textSizeNormal + (textSizeSelected - textSizeNormal) * (1f - ratio); // 线性插值计算当前文字大小
tv.setTextSize(TypedValue.COMPLEX_UNIT_SP, textSize); // 应用文字大小(单位SP)
}
}
}