Android进阶之SeekBar动态显示进度

SeekBar滑动动态气泡实现
本文介绍了一种在Android开发中如何在原生SeekBar基础上添加一个跟随移动显示的气泡效果的方法,使用PopupWindow和ConstraintLayout实现,改动小且不影响原始控件,提供了Kotlin和Java的示例代码。

SeekBar 在开发中并不陌生,默认的SeekBar是不显示进度的,当然用吐司或者文案在旁边实时显示也是可以的,那能不能移动的时候才显示,默认不显示呢,当然网上花哨的三方类太多了,但是我只是单纯的想在SeekBar的基础上去添加一个可以跟随移动显示的气泡而已~

先看一下效果:

在这里插入图片描述

这篇文章可能会满足你的需求
1.原生SeekBar使用,无需重写
2.改动量少,不会对控件有任何影响
3.使用灵活, Utils使用,复制粘贴即可使用
4.样式灵活,自己编写样式

先说一下原理吧:
1.首先最最基础的就是怎么样在不做到对原有控件产生影响的情况下去显示呢?
答: PopupWindow,它只需要拿到对应的目标控件即可指定显示位置
2.如何去跟随移动呢?
答:PopupWindow本身不会动态移动,只需要在该弹窗里面设置一个控件,让该控件移动即可

具体实现
拿到控件,用PopupWindow显示在该控件附近,根据SeekBar的进度,动态设置该弹窗里面子控件的位置

使用

这里是SeekBar移动监听,在这里的三个方法加上对应的方法即可

        mDataBind.controlVolumeSeekbar.setOnSeekBarChangeListener(object: SeekBar.OnSeekBarChangeListener{
            override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
                //滑块移动

                SeekBarPopUtils.move(progress,seekBar!!)
            }

            override fun onStartTrackingTouch(seekBar: SeekBar?) {
                //滑块按下
                SeekBarPopUtils.showPop(seekBar!!)
            }

            override fun onStopTrackingTouch(seekBar: SeekBar?) {
                //滑块松开
                SeekBarPopUtils.dismiss()
            }
        })

SeekBarPopUtils (Kotlin)

这里的 tvPopTxt 就是上图的小气泡,如果需要其他样式,直接去弹窗布局里面编写即可

/**
 * SeekBar移动时弹出对应的气泡加数字*/
@SuppressLint("StaticFieldLeak")
object SeekBarPopUtils {

    private var popWin: PopupWindow? = null
    private var clPopPar: ConstraintLayout? = null
    private var tvPopTxt: TextView? = null

    fun showPop(seekBar: SeekBar){
        popWin = PopupWindow()
        val mPopView = LayoutInflater.from(BaseApplication.getContext()).inflate(R.layout.item_popup_win,null,false)
        clPopPar = mPopView.findViewById<ConstraintLayout>(R.id.cl_pop_par)
        tvPopTxt = mPopView.findViewById<TextView>(R.id.tv_pop_txt)

        popWin?.contentView = mPopView
        popWin?.height = AppHelper.dp2px(30)
        popWin?.width = seekBar.width
        popWin?.showAsDropDown(seekBar,0,-(AppHelper.dp2px(30) + popWin!!.height))
    }


    fun move(progress: Int,seekBar: SeekBar){
        if (clPopPar != null){
            val tvPopWidth = AppHelper.dp2px(40)
            val params: ConstraintLayout.LayoutParams = ConstraintLayout.LayoutParams(
                tvPopWidth, AppHelper.dp2px(30)
            )
            params.startToStart = clPopPar!!.id
            params.marginStart = (seekBar.width - tvPopWidth)/100 * progress + tvPopWidth/3
            tvPopTxt?.layoutParams = params

            tvPopTxt?.text = progress.toString()
        }
    }


    fun dismiss(){
        popWin?.dismiss()
        popWin = null
        clPopPar = null
        tvPopTxt = null
    }

}

SeekBarPopUtils (Java)

public class SeekBarPopUtils {
   
   

    private static PopupWindow popWin = null;
    
<think>我们正在解决一个Android开发中的问题:在Adapter中使用SeekBar时,滑动过程中进度条出现跳变。问题分析:在列表或RecyclerView的Adapter中,SeekBar通常用于每个列表项。由于列表项会被回收重用,如果不正确处理SeekBar的状态,就会导致进度跳变。解决方案:我们需要在Adapter中正确保存和恢复SeekBar进度状态,并注意处理事件冲突。步骤:1.在数据模型中保存进度值:每个列表项对应的数据对象应该有一个字段存储SeekBar进度。2.在Adapter的onBindViewHolder中,将数据模型中的进度值设置给SeekBar。3.为SeekBar设置OnSeekBarChangeListener,并在进度改变时更新数据模型中的进度值。4.注意:由于RecyclerView的复用机制,当用户滑动SeekBar时,如果列表项被回收然后复用到其他位置,如果不保存进度,那么新位置会显示之前设置的进度,导致跳变。但是,仅仅这样还不够,因为滑动过程中如果列表项被复用,我们需要确保进度值被正确更新到数据模型,并且当该列表项再次出现时,从数据模型中读取正确的进度值。另外,为了避免事件冲突,我们可能需要设置RecyclerView不拦截SeekBar的触摸事件。具体实现:在Adapter中,我们通常这样处理:1.定义数据模型类,包含进度字段:```javapublic classItem {privateint progress; //保存SeekBar进度//其他字段...publicint getProgress(){ returnprogress;}public voidsetProgress(int progress) {this.progress =progress;}}```2.在ViewHolder中初始化SeekBar并设置监听器:```javapublicclass ViewHolder extendsRecyclerView.ViewHolder{privateSeekBarseekBar;private Itemitem;//当前ViewHolder绑定的数据对象public ViewHolder(ViewitemView) {super(itemView);seekBar= itemView.findViewById(R.id.seekBar);seekBar.setOnSeekBarChangeListener(newSeekBar.OnSeekBarChangeListener() {@Overridepublicvoid onProgressChanged(SeekBarseekBar, intprogress,boolean fromUser){if (fromUser) {//当用户拖动改变进度时,更新数据对象item.setProgress(progress);}}@Overridepublicvoid onStartTrackingTouch(SeekBar seekBar){}@Overridepublic voidonStopTrackingTouch(SeekBarseekBar) {}});}//绑定数据的方法public voidbind(Item item) {this.item =item;seekBar.setProgress(item.getProgress());//绑定其他数据...}}```3.在Adapter的onBindViewHolder中:```java@Overridepublicvoid onBindViewHolder(ViewHolderholder,int position) {Itemitem =itemList.get(position);holder.bind(item);}```但是,上述做法在快速滑动列表时,由于ViewHolder被复用时,新的数据会立即设置到SeekBar上,导致进度条突然变化(跳变)。为了解决这个问题,我们需要在复用ViewHolder时,确保在设置进度之前移除监听器,避免在设置进度时触发监听器中的逻辑(因为此时可能不是用户操作)。改进:在ViewHolder的bind方法中,我们先移除监听器,设置进度,再重新设置监听器:```javapublic voidbind(Item item) {this.item =item;//先移除监听器,避免在设置进度时触发onProgressChangedseekBar.setOnSeekBarChangeListener(null);seekBar.setProgress(item.getProgress());seekBar.setOnSeekBarChangeListener(seekBarChangeListener);//重新设置监听器// ...其他绑定}```注意:这里我们定义了一个成员变量seekBarChangeListener来保存监听器,避免重复创建。另一种常见做法是:在onBindViewHolder中,我们除了设置进度外,还要注意在每次绑定时更新监听器中的item引用,确保更新的是正确的数据对象。但是,上述改进后,跳变问题通常可以解决。然而,如果用户正在拖动SeekBar,此时列表项被回收,那么当用户松手时,由于ViewHolder已经被绑定到新的数据,那么onStopTrackingTouch事件可能不会触发,或者触发到错误的项。这通常不会造成严重问题,因为我们在onProgressChanged中已经更新了数据。此外,我们还需要处理RecyclerView的滚动事件和SeekBar的拖动事件之间的冲突。默认情况下,当用户垂直滑动SeekBar时,可能会触发RecyclerView的滚动。为了避免这种情况,我们可以在SeekBar开始触摸时禁止RecyclerView滚动,在结束触摸时允许滚动。在ViewHolder的监听器中:```javaseekBar.setOnSeekBarChangeListener(newSeekBar.OnSeekBarChangeListener() {@Overridepublicvoid onStartTrackingTouch(SeekBarseekBar) {//当开始拖动SeekBar时,禁止RecyclerView滚动recyclerView.requestDisallowInterceptTouchEvent(true);}@Overridepublic voidonStopTrackingTouch(SeekBarseekBar) {//结束拖动时,允许RecyclerView滚动recyclerView.requestDisallowInterceptTouchEvent(false);}// ...onProgressChanged});```注意:recyclerView需要从ViewHolder中获取,可以通过在ViewHolder中保存一个recyclerView的引用(在构造时传入),或者通过itemView.getParent()获取,但要注意类型。更好的方式:在Adapter中提供一个设置RecyclerView的方法,或者在ViewHolder中通过itemView的上下文来获取RecyclerView(不推荐,因为可能获取不到)。我们可以在创建ViewHolder时将RecyclerView传递进去。然而,在RecyclerView.Adapter中,我们可以通过ViewHolder的getBindingAdapterPosition()等方法获取位置,但事件冲突处理通常需要直接操作RecyclerView。实际上,我们可以通过ViewHolder的itemView的父视图来获取RecyclerView:```javaRecyclerView recyclerView =(RecyclerView) itemView.getParent();```但是,当itemView还没有被添加到RecyclerView时,getParent()可能返回null。因此,我们可以在onStartTrackingTouch中这样写:```javaViewParentparent =itemView.getParent();if (parent instanceofRecyclerView) {parent.requestDisallowInterceptTouchEvent(true);}```同样,在onStopTrackingTouch中:```javaViewParentparent =itemView.getParent();if (parent instanceofRecyclerView) {parent.requestDisallowInterceptTouchEvent(false);}```总结:通过以上步骤,我们可以解决在Adapter中SeekBar进度跳变的问题:1.在数据模型中保存进度。2.在ViewHolder的bind方法中,先移除监听器,设置进度,再添加监听器。3.处理触摸事件冲突,在开始拖动时禁止父视图拦截事件,结束拖动时允许。注意:如果列表项中有多个SeekBar,需要分别处理。引用说明:关于SeekBar的样式设置(如离散式SeekBar)和竖直方向设置,可以参考引用[1]中的内容,但本问题主要关注跳变问题,因此不展开。参考引用[1]中提到的离散式SeekBar(style="@style/Widget.AppCompat.SeekBar.Discrete")适用于固定节点,但本问题中的跳变与是否离散无关,因此上述解决方案同样适用。最后,提供完整代码示例(简化版):数据模型:```javapublicclass Item{private intprogress;//...其他字段}``` Adapter和ViewHolder:```javapublicclass MyAdapter extendsRecyclerView.Adapter<MyAdapter.ViewHolder> {privateList<Item>itemList;// ...其他方法@Overridepublic ViewHolder onCreateViewHolder(ViewGroup parent, intviewType) {Viewview =LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item,parent,false);returnnew ViewHolder(view);}@Overridepublic voidonBindViewHolder(ViewHolder holder, intposition){Item item= itemList.get(position);holder.bind(item);}public staticclass ViewHolder extendsRecyclerView.ViewHolder{private SeekBar seekBar;privateItem item;public ViewHolder(ViewitemView) {super(itemView);seekBar= itemView.findViewById(R.id.seekBar);//初始化监听器seekBar.setOnSeekBarChangeListener(seekBarChangeListener);}publicvoid bind(Itemitem){this.item= item;//先移除监听器,避免设置进度时触发seekBar.setOnSeekBarChangeListener(null);seekBar.setProgress(item.getProgress());//重新设置监听器seekBar.setOnSeekBarChangeListener(seekBarChangeListener);}private finalSeekBar.OnSeekBarChangeListenerseekBarChangeListener= newSeekBar.OnSeekBarChangeListener() {@Overridepublicvoid onProgressChanged(SeekBarseekBar, intprogress,boolean fromUser){if (fromUser) {item.setProgress(progress);}}@Overridepublic voidonStartTrackingTouch(SeekBarseekBar) {//禁止父视图拦截触摸事件ViewParent parent= itemView.getParent();if(parent!= null) {parent.requestDisallowInterceptTouchEvent(true);}}@Overridepublic voidonStopTrackingTouch(SeekBarseekBar) {ViewParent parent= itemView.getParent();if(parent!= null) {parent.requestDisallowInterceptTouchEvent(false);}}};}}```这样,应该能够解决SeekBar在滑动时进度跳变的问题。相关问题:1.在RecyclerView中,如何处理其他可交互视图(如CheckBox、Switch)的状态保存?2.如何优化RecyclerView的Adapter性能?3.在RecyclerView中,如何实现多个视图类型(包括不同的交互控件)?</think>### 解决 Android Adapter 中 SeekBar 进度跳变问题 在 Android Adapter 中使用 SeekBar 时出现进度跳变,通常是由 **视图复用机制** 和 **进度状态未正确保存** 导致的。以下是完整解决方案: #### 核心原因分析 1. **RecyclerView 的视图复用机制** 当滑动列表时,被移出屏幕的 item 会被复用到新位置。如果未正确处理 SeekBar 状态,新位置的进度值会被重置。 2. **进度值未绑定数据模型** SeekBar进度值未与数据源关联,导致 `onBindViewHolder()` 重置进度。 3. **事件冲突** RecyclerView 的滚动事件与 SeekBar 的拖动事件冲突。 --- #### 完整解决方案(Kotlin 实现) ##### 1. 数据模型绑定进度值 ```kotlin data class ListItem( val id: Int, var progress: Int // 必须为 var 确保可更新 ) ``` ##### 2. ViewHolder 正确处理状态 ```kotlin class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { private val seekBar: SeekBar = itemView.findViewById(R.id.seekBar) private var currentItem: ListItem? = null init { seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { // 仅保存用户操作产生的进度变化 if (fromUser) { currentItem?.progress = progress } } override fun onStartTrackingTouch(seekBar: SeekBar) { // 禁止父容器拦截事件 (itemView.parent as? RecyclerView)?.requestDisallowInterceptTouchEvent(true) } override fun onStopTrackingTouch(seekBar: SeekBar) { (itemView.parent as? RecyclerView)?.requestDisallowInterceptTouchEvent(false) } }) } fun bind(item: ListItem) { currentItem = item // 关键:移除监听器避免递归调用 seekBar.setOnSeekBarChangeListener(null) seekBar.progress = item.progress seekBar.setOnSeekBarChangeListener(seekBarChangeListener) } } ``` ##### 3. Adapter 中实现稳定绑定 ```kotlin override fun onBindViewHolder(holder: ViewHolder, position: Int) { val item = dataList[position] holder.bind(item) } // 解决快速滑动时的状态不一致 override fun onViewRecycled(holder: ViewHolder) { holder.seekBar.setOnSeekBarChangeListener(null) super.onViewRecycled(holder) } ``` ##### 4. 解决事件冲突(XML) ```xml <!-- 确保 RecyclerView 不拦截事件 --> <androidx.recyclerview.widget.RecyclerView android:id="@+id/recyclerView" android:descendantFocusability="beforeDescendants" android:overScrollMode="never"/> ``` --- #### 关键优化点 1. **状态分离** 将进度值存储在数据模型中,而非依赖 View 的状态。 2. **监听器动态管理** 在 `bind()` 时临时移除监听器,避免 `setProgress()` 触发错误回调。 3. **事件拦截控制** 拖动时禁止 RecyclerView 滚动: ```kotlin (itemView.parent as? RecyclerView)?.requestDisallowInterceptTouchEvent(true) ``` 4. **内存泄漏防护** 在 `onViewRecycled()` 中解绑监听器[^1]。 --- #### 进阶方案:使用 DataBinding ```kotlin // ViewModel 中 val progress = MutableLiveData<Int>() // XML 中 <SeekBar android:progress="@={viewModel.progress}" app:onProgressChanged="@{viewModel::onProgressChanged}" /> ``` > **提示**:若需固定进度节点(如仅允许 0/25/50/75/100),参考引用[1]使用 `style="@style/Widget.AppCompat.SeekBar.Discrete"` --- ### 相关问题 1. 如何在 RecyclerView 中正确处理 CheckBox 的状态保存问题? 2. RecyclerView 的视图复用机制会导致哪些常见问题?如何系统解决? 3. 如何实现 AndroidSeekBar 的垂直方向显示? 4. 当 RecyclerView 嵌套 ScrollView 时,如何解决滑动冲突? [^1]: 引用来源: Android 官方文档 - RecyclerView 最佳实践
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值