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 层的表现都一样丝滑流畅。

相关推荐
特立独行的猫a2 小时前
从XML到Compose的UI变革:现代(2026)Android开发指南
android·xml·ui·compose·jetpack
xiangxiongfly9152 小时前
Android 共享元素转场效果
android·动画·共享元素转场效果
我是阿亮啊2 小时前
Android 中线程和进程详解
android·线程·进程·进程间通信
我命由我123453 小时前
Android 开发问题:Duplicate class android.support.v4.app.INotificationSideChannel...
android·java·开发语言·java-ee·android studio·android-studio·android runtime
似霰3 小时前
Android 平台智能指针使用与分析
android·c++
有位神秘人3 小时前
Android中BottomSheetDialog的折叠、半展开、底部固定按钮等方案实现
android
LeeeX!3 小时前
YOLOv13全面解析与安卓平台NCNN部署实战:超图视觉重塑实时目标检测的精度与效率边界
android·深度学习·yolo·目标检测·边缘计算
dongdeaiziji3 小时前
Android 图片预加载和懒加载策略
android
一起养小猫4 小时前
Flutter for OpenHarmony 实战:科学计算器完整开发指南
android·前端·flutter·游戏·harmonyos
帅得不敢出门4 小时前
Android定位RK编译的system.img比MTK大350M的原因
android·framework·策略模式