Android 自定义 View 实战:打造一个跟随滑动的丝滑指示器

在 Android 开发中,我们经常需要为 RecyclerViewViewPagerHorizontalScrollView 添加一个可视化的滚动指示器。虽然系统自带的 ScrollBar 能满足基本需求,但如果 UI 设计要求指示器有固定的宽度、圆角以及特定的颜色,自定义 View 往往是最佳选择。

本文将带你使用 Kotlin 实现一个轻量级、高性能的水平滑动指示器组件 BottomLineView,并详细讲解如何适配主流的滚动控件。

1. 核心组件实现:BottomLineView

我们创建一个继承自 View 的类 BottomLineView。它不依赖具体的滚动控件,只接收指示器宽度滚动比例两个参数,实现了高度解耦。

kotlin 复制代码
class BottomLineView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

    private lateinit var paintGray: Paint  // 背景线画笔
    private lateinit var paintGreen: Paint // 指示器画笔
    
    private var indicatorWidth = 0f // 指示器的固定宽度
    private var currentOffset = 0f  // 当前偏移量

    init {
        initPaint()
    }

    private fun initPaint() {
        // 背景线:灰色,圆角
        paintGray = Paint().apply {
            color = -0x333334 // 0xFFCCCCCC
            strokeCap = Paint.Cap.ROUND
            strokeWidth = 10f
            isAntiAlias = true
        }
        // 指示器:绿色,圆角
        paintGreen = Paint().apply {
            color = -0xff0100 // 0xFF00FF00
            strokeCap = Paint.Cap.ROUND
            strokeWidth = 10f
            isAntiAlias = true
        }
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        val centerY = height / 2f
        val totalWidth = width.toFloat()

        // 1. 绘制背景线
        canvas.drawLine(0f, centerY, totalWidth, centerY, paintGray)

        // 2. 绘制指示器(确保不越界)
        val start = max(0f, min(currentOffset, totalWidth - indicatorWidth))
        val end = start + indicatorWidth

        if (indicatorWidth > 0) {
            canvas.drawLine(start, centerY, end, centerY, paintGreen)
        }
    }

    /** 设置指示器物理宽度 */
    fun setIndicatorWidth(width: Float) {
        this.indicatorWidth = width
        invalidate()
    }

    /** 更新滚动比例 (0.0 ~ 1.0) */
    fun updateScrollRatio(ratio: Float) {
        val maxOffset = width - indicatorWidth
        this.currentOffset = maxOffset * ratio
        invalidate()
    }
}

2. 场景适配指南

下面是三种最常见的滚动控件适配方案。

场景一:与 RecyclerView 联动

这是最常用的场景。利用 computeHorizontalScrollXXX 系列方法可以精确计算滚动位置。

kotlin 复制代码
// 1. 设置指示器宽度
bottomLineView.setIndicatorWidth(100f)

// 2. 监听滚动
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
    override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
        // 获取滚动参数
        val offset = recyclerView.computeHorizontalScrollOffset() // 当前偏移
        val range = recyclerView.computeHorizontalScrollRange()   // 内容总宽
        val extent = recyclerView.computeHorizontalScrollExtent() // 可见宽度
        
        // 计算最大可滚动距离
        val maxScroll = range - extent
        
        if (maxScroll > 0) {
            val ratio = offset.toFloat() / maxScroll
            bottomLineView.updateScrollRatio(ratio)
        }
    }
})

场景二:与 HorizontalScrollView 联动

HorizontalScrollView 需要 API 23 (Android 6.0) 以上才能通过 setOnScrollChangeListener 监听。如果是低版本,可能需要继承并重写 onScrollChanged

kotlin 复制代码
horizontalScrollView.setOnScrollChangeListener { _, scrollX, _, _, _ ->
    // 1. 获取子 View (内容)
    val contentView = horizontalScrollView.getChildAt(0) ?: return@setOnScrollChangeListener
    
    // 2. 计算最大可滚动距离 = 内容宽度 - 容器宽度
    val maxScroll = contentView.width - horizontalScrollView.width
    
    if (maxScroll > 0) {
        val ratio = scrollX.toFloat() / maxScroll
        bottomLineView.updateScrollRatio(ratio)
    }
}

场景三:与 ViewPager2 联动

ViewPager2 的计算逻辑略有不同,因为它是按"页"滚动的。

kotlin 复制代码
viewPager2.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
    override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
        super.onPageScrolled(position, positionOffset, positionOffsetPixels)
        
        // 假设 Adapter 中有 itemCount
        val itemCount = adapter.itemCount
        
        if (itemCount > 1) {
            // 计算总进度
            // 当前页索引 + 当前页偏移百分比
            val currentProgress = position + positionOffset
            
            // 总的可移动页数 = 总页数 - 1
            val totalProgress = (itemCount - 1).toFloat()
            
            val ratio = currentProgress / totalProgress
            bottomLineView.updateScrollRatio(ratio)
        }
    }
})

3. 总结

通过将 UI 绘制逻辑封装在 BottomLineView 内部,并将状态更新抽象为 updateScrollRatio(0.0~1.0) 接口,我们成功地让这个指示器适配了 Android 所有的水平滚动组件。无论底层是用 RecyclerView 还是 ViewPager,UI 层的表现都一样丝滑流畅。

相关推荐
李艺为3 小时前
Fake Device Test作假屏幕分辨率分析
android·java
zh_xuan3 小时前
github远程library仓库升级
android·github
峥嵘life3 小时前
Android蓝牙停用绝对音量原理
android
小书房4 小时前
Kotlin的内联函数
java·开发语言·kotlin·inline·内联函数
czlczl200209255 小时前
IN和BETWEEN在索引效能的区别
android·adb
Volunteer Technology5 小时前
ES高级搜索功能
android·大数据·elasticsearch
北京自在科技5 小时前
Find Hub App 小更新
android·ios·安卓·findmy·airtag
lbb 小魔仙5 小时前
2026远程办公软件夏季深度横测:ToDesk、向日葵、网易UU远程全面对比,远控白皮书
android·服务器·网络协议·tcp/ip·postgresql
coding_fei6 小时前
AudioServer初始化过程
android
brucelee1866 小时前
Docker 运行 Android 模拟器
android·docker·容器