news 2026/10/17 19:19:43

用 LinearGradient 给 Android TextView 实现卡拉OK歌词高亮

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用 LinearGradient 给 Android TextView 实现卡拉OK歌词高亮

简介:面向Android开发者的歌词风格渐变文字自定义View实现方案,基于Gradient渐变机制,让TextView文字颜色随位置逐步过渡,解决普通TextView文字颜色单一、缺乏动态视觉表现的问题。资源包共39个文件,以Java源码、XML布局与配置、Gradle构建文件为主,另含PNG素材、项目说明与辅助脚本,完整呈现GradientTextView从JitPack仓库依赖配置、XML布局绑定到Java代码调用的全链路实现。包体仅101KB,结构紧凑且分层清晰,app与mylibrary模块分离,适合中初级开发者快速理解自定义View的测量、绘制与LinearGradient应用。已有495人学习参考,可直接借鉴该组件的双色渐变方案,用于音乐播放器歌词滚动、字幕展示、标题强调等界面场景,减少重复开发成本,提升文字动效质感。资源包适合直接作为Gradle依赖引入现有工程,也可将核心绘制代码抽取到自有项目中,随附的proguard规则与示例工程便于排查混淆与编译问题。

1. 歌词风格 TextView:一个用 Gradient 渐变实现的卡拉 OK 高亮文本

做播放器类 App 时,最头疼的需求之一就是「歌词逐句高亮」。普通 TextView 只能整体变颜色,做不到一句歌词里前半句亮、后半句暗的卡拉 OK 扫描效果。网上搜「歌词 TextView」,搜出来的大多是基于自定义 View 加 Paint 手动逐字绘制,实现复杂且性能不好控。而这份源码给了一个思路相对清爽的方案:不改动 TextView 的绘制框架,利用 LinearGradient 渐变加 Shader 矩阵平移,让文字本身产生「已播放部分亮、未播放部分暗」的过渡效果。核心很简单:文字颜色不是 setTextColor,而是通过 Paint 的 Shader 去控制;高亮进度变化时只需要平移 Shader 的坐标矩阵,重绘成本极低,不涉及逐字截断或 Path 测量。这份资源适合做音乐播放器、短视频字幕、朗读跟读场景的开发者,直接拿去改就能用。如果你正被歌词高亮和逐句字幕的实现卡住,这篇笔记值得一读。

2. 技术定位与选型:为什么用 Gradient 而不是逐字重绘

2.1 歌词高亮的主流实现方案对比

歌词逐句高亮,业界比较常见的做法有三类。第一类是把一句歌词拆成多个 TextView 或 Spannable,逐字设置颜色,配合 Handler 或 Choreographer 逐帧刷新。这种方案最直观,但问题也很明显:中英文混排时字符宽度不一致,换行时高亮位置会错位;Spannable 每帧都要重建,频繁分配对象容易触发 GC 掉帧。第二类是用 Canvas 直接绘制文本,通过 Paint 测量每个字符的宽度,在 onDraw 里手动算出哪些字亮哪些字暗。这类方案可控性强,但代码量很大,要处理 textWidth、baseline、换行、行间距,稍微粗心就会出现字幕对不齐的玄学问题。第三类就是这份源码采用的思路:单行或单句文字整体用 LinearGradient 上色,渐变两端颜色分别代表「已播放」和「未播放」,高亮进度对应渐变的位置比例。每次刷新只需要改渐变矩阵,不用重建文本布局,绘制开销非常小。

从工程角度讲,第三类方案最符合「能用简单结构解决就不上复杂结构」的原则。逐字重绘的方案在歌词行数多、频繁切换的场景下,每帧要重新测量和绘制几十甚至上百个字符,而渐变方案每次只是移动一个矩阵,GPU 和 CPU 的负载都低得多。尤其当需求是「整句渐变扫描」而不是「每个字逐一跳动」时,渐变方案的视觉连贯性也更好,不会出现字与字之间颜色断层的效果。

2.2 Gradient 方案的绘制链路

这份源码里,渐变高亮的绘制链路大致是:在 onSizeChanged 里根据 View 宽度创建 LinearGradient,初始时让渐变色从「高亮色」到「暗色」完整覆盖文本区域;然后在刷新进度时,通过 Matrix 的 postTranslate 平移渐变位置。平移量跟播放进度成正比,进度为 0 时整个文本都是暗色,进度为 100% 时整个文本都是亮色。因为 Shader 是画在 Paint 上的,TextView 的 onDraw 会拿这个 Paint 去绘制文本,所以文字颜色的变化完全是 Shader 的渐变效果在起作用。

这套方案有一个天然优势:文本的绘制、换行、测量逻辑依然是 TextView 原生行为,不用自己处理复杂的排版。你只需要维护一个「当前行歌词的播放进度」,然后计算对应的平移偏移量,调用 invalidate 触发重绘即可。对比逐字重绘方案,代码量可以减少一半以上,而且踩坑点少很多,基本不存在测量误差导致的高亮错位问题。

2.3 技术栈与适用边界

从源码工程结构看,整体是标准的 Android 项目,核心代码集中在自定义 View 类里,不依赖第三方库,最低 API 级别按 Android 5.0 以上处理就没有兼容问题。Gradient 相关的核心类是 android.graphics.LinearGradient 和 android.graphics.Matrix,外加 Paint.setShader 和 Canvas.drawText 的常规调用。工程里附带了一个演示用的播放进度模拟器,方便在没有播放器数据源的情况下预览效果。

需要提醒的是,这个方案最适合「一行一句」的歌词布局。如果歌词特别长导致 TextView 内部滚动,或者需要同时显示多行并由播放进度驱动各行独立高亮,那就要在外部做行管理,把当前播放到第几句的索引计算好,再传给这个自定义 View。源码里对这种情况有预留接口,但主要精力放在单行渐变上。做多行歌词界面时,常见做法是用 RecyclerView 或 LinearLayout 包多行这个 View,当前行渐变高亮,其他行普通颜色,代码层面加一个 setActive(boolean) 标记即可。

3. 工程结构与核心类:源码包里的文件布局

3.1 目录结构与职责划分

解压源码包之后,典型的 Android 工程目录如下,我会按「从外到内」的顺序说明每个目录的作用:

app/ ├── src/main/ │ ├── java/com/example/lyrictextview/ │ │ ├── LyricTextView.java // 自定义歌词 TextView 的核心类 │ │ ├── LyricProgressHelper.java // 进度管理辅助类 │ │ ├── MainActivity.java // 演示页面,模拟进度驱动 │ │ └── ...其他辅助文件 │ ├── res/ │ │ ├── layout/ │ │ │ └── activity_main.xml // 演示布局,直接引用 LyricTextView │ │ └── values/ │ └── AndroidManifest.xml ├── build.gradle └── gradle/

LyricTextView.java 是这份源码的核心文件,所有渐变绘制逻辑都集中在这里,大概两百多行。它继承自 AppCompatTextView(也可以直接继承 TextView,取决于工程是否启用 AndroidX),重写了 onSizeChanged、onDraw 两个方法,并对外暴露 setProgress(float) 和 setProgressColor(int) 两个关键接口。LyricProgressHelper 负责把播放器传过来的时间戳转换成 0~1 之间的进度值,这样 LyricTextView 只关心进度,不需要知道 MP3 时长、歌词时间戳这类业务数据,是一个很有价值的解耦设计。

MainActivity 是演示用的宿主页面。它内置了一个 Handler 定时器,每 16 毫秒生成一个递增的模拟进度值,然后调 LyricTextView.setProgress(),让我们不接播放器就能直接看到渐变扫描效果。这个页面还放了一个 SeekBar,可以手动拖拽进度观察渐变色块的位置变化,非常适合快速验证核心效果。

3.2 LyricTextView 的内部状态设计

这个类的内部状态设计值得细说,因为它直接影响后续二次开发的扩展方式。核心状态有三个:mGradient 存放 LinearGradient 实例,mMatrix 存放渐变平移矩阵,mProgress 存放当前高亮进度(0 到 1 的浮点数)。另外还有两个颜色值 mHighlightColor 和 mBaseColor,分别代表高亮色和未播放时的暗色。

设计上有一个比较聪明的点:渐变对象在 onSizeChanged 时创建,因为 LinearGradient 依赖视图宽度来计算颜色分布坐标。如果 View 宽度是 0 时就去创建,渐变会变成一个退化的色块,绘制出来完全不对。源码里对这种情况做了防御性判断,mGradient 为空时直接走 super.onDraw,保证 View 在初始化阶段不会崩溃。这个细节在后面排查问题时要特别注意,后面第 5 章会展开讲。

3.3 对外接口与调用方式

调用方只需要关心两个入口。第一个是 setProgress(float progress),参数范围 0~1,0 表示整句未唱,1 表示整句已唱完,内部会做 clamp 防越界。第二个是 setProgressColor(int color),用来设置高亮颜色,底色在 XML 里通过 android:textColor 属性控制。当控件处于非激活状态(比如歌词还没唱到这一句)时,调用 setActive(false) 可以让它恢复成普通 TextView 显示,不会残留渐变效果。

辅助类 LyricProgressHelper 的作用是把「当前播放时间」和「歌词起止时间」两个时间戳换算成进度。换算公式很简单:progress = (currentTime - startTime) / (endTime - startTime)。注意处理分母为 0 的边界情况,源码里做了保护,不会出现除零或无限大的进度值。这个类单独拆出来的好处是:未来接入真实播放器时,你只需要替换数据来源,不需要动 LyricTextView 的绘制逻辑。

4. 核心实现拆解:从 onSizeChanged 到 onDraw 的完整高亮链路

4.1 初始化:在 onSizeChanged 里创建渐变

@Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); if (w > 0) { // 创建水平方向的线性渐变 // 左侧第 1 个颜色 stop 是基色(未播放),右侧第 2 个颜色 stop 是高亮色(已播放) // Shader.TileMode.CLAMP 表示渐变范围外的区域使用边缘颜色填充 mGradient = new LinearGradient( 0, 0, w, 0, new int[]{mBaseColor, mHighlightColor}, new float[]{0f, 1f}, Shader.TileMode.CLAMP ); mMatrix = new Matrix(); } }

逻辑说明:LinearGradient 的四个坐标参数(x0, y0, x1, y1)决定了渐变的方向和范围。这里 x0=0、x1=w,y 坐标都是 0,所以是一条水平渐变,从左到右颜色从 mBaseColor 过渡到 mHighlightColor。Shader.TileMode.CLAMP 的作用是:当渐变的实际绘制范围超过 0 到 w 这个区间时,超出部分用渐变边缘的颜色填充,不会产生重复平铺的条纹。

参数细节:colors 数组里我按照「左暗右亮」的顺序传入 mBaseColor 和 mHighlightColor。如果你想把高亮放在左侧、底色放在右侧,也就是「从右往左唱」的效果,只需要交换 colors 数组的顺序,或者把 x0 改成 w、x1 改成 0。positions 数组的 {0f, 1f} 表示第一个颜色在渐变起点、第二个颜色在渐变终点,默认等分即可,不需要每个颜色都指定位置。

4.2 进度驱动:通过矩阵平移实现渐变扫描

public void setProgress(float progress) { // 限制进度在 0~1 之间,防止外部传参异常 mProgress = Math.max(0f, Math.min(1f, progress)); if (mGradient != null) { // 平移矩阵:让渐变整体向右移动 (1 - progress) * 宽度 // 当 progress=0 时,渐变整体向左移出视野,文字显示为边缘底色(暗色) // 当 progress=1 时,渐变完全覆盖文字区域,文字显示为高亮色 mMatrix.reset(); float offset = (1f - mProgress) * getWidth(); mMatrix.postTranslate(-offset, 0f); mGradient.setLocalMatrix(mMatrix); } invalidate(); }

逻辑说明:这里用的是反向平移的思路。渐变初始覆盖范围是 0 到 w,颜色分布是左暗右亮。如果直接把这个渐变画上去,整行文字会同时呈现左暗右亮的状态,没有扫描效果。所以我们要做的是:让渐变整体向左移动,移动的距离是 (1 - progress) * w。当 progress 为 0 时,偏移量是 w,渐变整体左移一个宽度,原本 0 到 w 的渐变区域完全移出可视区,CLAMP 模式下文字区域显示的是渐变左侧边缘的基色,也就是暗色;当 progress 慢慢增大,渐变右移,文字从左到右逐渐进入渐变区域,先进入的部分是渐变的中间过渡色再到高亮色,看起来就是「从左到右逐字点亮」的效果;当 progress 等于 1 时,偏移量为 0,渐变完整覆盖文字区,整句变成高亮色。

这个思路的核心价值在于:每次进度刷新,只是重置 Matrix 并调用 setLocalMatrix,不涉及任何文本重排、字符测量或颜色重新计算。Android 底层的 Shader 矩阵变换是 GPU 加速的,性能开销可以忽略。你可以用 Choreographer 每帧调 setProgress,也完全扛得住;而逐字重绘方案在这种频率下往往已经出现掉帧了。

4.3 绘制:onDraw 中应用带 Shader 的 Paint

@Override protected void onDraw(Canvas canvas) { if (mGradient != null) { // 备份旧的 Paint 颜色,绘制完后恢复,避免影响系统其他绘制逻辑 int oldColor = getPaint().getColor(); // 把渐变 Shader 设置到 TextView 的 Paint 上 getPaint().setShader(mGradient); // 调用 super.onDraw 让原生 TextView 用这个 Paint 绘制文本 super.onDraw(canvas); // 绘制完成后移除 Shader,恢复原状 getPaint().setShader(null); getPaint().setColor(oldColor); } else { super.onDraw(canvas); } }

逻辑说明:这是整个方案最巧妙也最容易出问题的地方。TextView 的文字颜色本来由 Paint 的 color 属性决定,但 Paint 还支持设置 Shader,一旦设置了 Shader,文字颜色就完全由 Shader 的颜色分布决定,color 属性被忽略。我们利用这个特性,在 onDraw 前把 mGradient 塞进 TextView 内部的 Paint,然后调用 super.onDraw(canvas),让 TextView 原生的绘制流程去画文字。这样文字颜色就是渐变效果,而排版、换行、行间距、字间距这些逻辑完全走系统原生实现。

注意一个细节: TextView 内部的 Paint 对象是共享复用的,不恢复的话会影响下一次绘制,甚至影响同一 View 内的其他绘制操作(比如光标、装饰线)。所以绘制完成后必须 setShader(null) 并恢复原 color。常见做法是 try-finally 包裹,万一一帧绘制抛异常也能保证状态恢复。源码里虽然没写 try-finally,但二开时最好自己加上。

4.4 进度辅助类:时间戳到进度的换算

public class LyricProgressHelper { // 根据句子开始时间、结束时间和当前播放时间,计算 0~1 的进度 // 返回 -1 表示当前时间不在这一句的范围内 public float calculateProgress(long currentTime, long startTime, long endTime) { if (currentTime < startTime || currentTime > endTime) { return -1f; } long duration = endTime - startTime; if (duration <= 0) { // 防止时间戳数据异常导致除零 return 1f; } return (float) (currentTime - startTime) / (float) duration; } }

逻辑说明:这个辅助类解决的问题是「播放器的时间戳和歌词句子的时间节点如何映射到进度」。返回值是 0~1 的浮点数,可以直接传给 LyricTextView.setProgress()。返回值 -1 表示当前不在这个句子播放区间,此时外部逻辑可以把歌词行设为非激活状态。

参数细节:startTime 和 endTime 是歌词文件解析出来的句子起止时间,单位建议统一用毫秒;currentTime 是播放器的当前播放位置。有些播放器返回的是秒,记得先乘以 1000 再传进来,否则进度会跳得飞快。真实项目中歌词时间戳往往来自 LRC 文件解析,格式是分钟:秒.毫秒,换算时也统一到毫秒再比较,避免精度误差导致高亮提前或滞后半句。

4.5 演示页面的驱动逻辑

MainActivity 里的核心逻辑是一个 Handler 定时器,每 16ms 发一个空消息,收到消息后把 mSimulatedProgress 增加 0.005f,超过 1 就归零重来,然后调 LyricTextView.setProgress(mSimulatedProgress) 并更新进度条。这个模拟器的意义在于:不接真实播放器就能在布局里直接看效果,验证渐变方向和矩阵平移是否正确。接真实播放器时,把定时器换成播放器的进度回调即可,改动很小。

SeekBar 的监听也很直白:onProgressChanged 里把 SeekBar 的进度值除以最大值得到 0~1 的 progress,然后同步给 LyricTextView。这个交互对调试很有用,因为自动播放时进度变化很快,肉眼很难确认渐变位置是否准确;手动拖拽可以停在任意位置,仔细对照高亮边界和文字字符是否对齐。我一般会在二开时保留这个 SeekBar,它比任何日志都直观。

5. 踩坑清单与排查思路:跑起来之后遇到的 5 个典型问题

5.1 文字颜色没变化,依然是默认黑色

现象:把 LyricTextView 放进布局,调用 setProgress 后文字颜色没有任何变化,还是默认的黑色或 XML 里配的颜色。

原因:最常见的两种情况。一种是设置了 android:textColor 属性,TextView 在初始化时会用这个颜色去设置 Paint 的 color,然后在 onDraw 里 Shader 确实设置进去了,但某些机型或主题下 TextView 的 onDraw 内部还有一次额外的颜色设置逻辑,把 Shader 覆盖掉了。另一种是自定义 View 里没有正确在绘制前 setShader,比如 mGradient 为空或者 setLocalMatrix 没生效。

解决:排查时先在 onDraw 入口处断点,确认 mGradient 非空;再检查 getPaint().setShader(mGradient) 是否执行到。大概率问题出在 textColor 的优先级上。我的习惯是:在 XML 里不要给这个自定义 View 设置 android:textColor,而是统一通过 setProgressColor 和构造参数传入颜色,避免系统属性和 Shader 冲突。如果确实需要默认颜色,在构造函数里用 setTextColor 是没有问题的,因为绘制阶段 Shader 会覆盖它。

5.2 进度刷新后渐变方向反了,高亮从右往左走

现象:歌词应该是从左往右点亮,实际运行时却是从右往左,或者一开始整句全亮,往后越来越暗。

原因:Matrix 平移方向和 LinearGradient 的颜色排列组合错了。源码里 LinearGradient 颜色是 base 在左、highlight 在右,矩阵向左平移实现扫描。如果你改动了 colors 数组顺序,或者调整了 x0、x1 的坐标方向,平移逻辑也要同步改。这是一个很容易被忽略的联动关系,改了渐变方向忘记改 Matrix。

解决:检查 LinearGradient 构造函数里 colors 数组和 positions 的对应关系。一条规律:想让高亮从文字左侧开始,就要让「高亮色所在的那一侧」随着 progress 增大逐渐进入可视区。如果用的是 base 左、highlight 右的渐变,就要向左平移让右侧高亮色区域逐渐滑入视野;如果渐变是 base 右、highlight 左,那就得向右平移。记不住规律时,直接把 progress 设为 0.5f 停在中间,看哪半边亮、哪半边暗,一眼就能定位。

5.3 高亮色和暗色之间过渡太生硬,像一条分割线

现象:渐变扫描效果是有了,但中间过渡带特别窄,亮和暗之间有明显分界,看起来不像歌词渐变,更像一刀切的遮罩。

原因:LinearGradient 的 positions 数组把两个颜色分配在 0f 和 1f,这意味着从起点到终点是完整的线性过渡。但如果 View 宽度很大,或者文字只占了左侧一小部分,渐变在空白区域就已经完成了大部分过渡,文字区域看到的自然是一段接近末端的纯色。本质上是渐变区域和文字内容的覆盖范围不匹配。

解决:有两个常见做法。一个是把 LinearGradient 的渐变范围从 w 改成文字实际占用的宽度,通过 Paint.measureText 或 StaticLayout 的 getLineWidth 拿到文本宽度,让渐变集中分布在文字区域。另一个是在 positions 里加一个中间 stop:比如 new float[]{0f, 0.7f, 1f},配三个颜色 {base, base, highlight},让前 70% 保持基色,后 30% 从基色过渡到高亮色,过渡会更柔和。实际项目中我喜欢两者结合:先量出文本宽度设置渐变范围,再用多 stop 控制过渡带宽窄。

5.4 快速切歌或拖动进度条时偶发闪一下全亮

现象:SeekBar 快速拖动时,歌词偶尔整体闪成高亮色,然后下一帧又恢复正常。

原因:当 progress 超过 1 或者刚好为 1 时,Matrix 平移量是 0,渐变完整覆盖文字区,文字全部是高亮色,这是正常形态。但快速拖动时进度值可能被 clamp 在 1,然后下一次刷新又回到 0.5,视觉上会有一个瞬间的「全亮」闪现。如果 progress 的来源是真实播放器,切歌时播放器的进度回调可能短暂上报一个异常大值,也会触发这个问题。

解决:在 setProgress 入口处对 progress 做更严格的防御,比如和上一次值做差值检查,超过 0.3 的跳变就丢弃这次刷新。另一种做法是:当 Helper 返回 -1 表示不在播放区间时,外层直接把这一行设为非激活状态,而不是把 -1 传给 setProgress。其次,如果你不希望进度为 1 时全亮(有些歌词 UI 要求唱完一句后渐变为暗色等待下一句),可以在 setProgress 里加一个判断:progress >= 1f 时渐变恢复为全 base 色而不是全 highlight 色。这个根据产品需求定,源码的默认行为是「唱完亮着」,如果你要做「唱完灭掉等下一句」,记得在进度为 1 时把 colors 临时反转。

5.5 多行歌词时,每行都渐变但同一进度传给了所有行

现象:把多行歌词都换成这个 LyricTextView 并传入同一个 progress,结果每一行都在做同样的渐变扫描,视觉上乱成一团。

原因:进度是「当前播放的这一句」的属性,不是所有行的公共属性。LyricTextView 本身是单行单句的设计,多行歌词的需求要靠外层容器管理行状态:当前播放的行传真实进度,其他行要么不传、要么设为非激活。源码的演示页面只展示了单行效果,多行需要你自行在 RecyclerView 或 LinearLayout 的绑定逻辑中做判断。

解决:设计上是给 LyricTextView 增加一个 setActive(boolean) 或者 setState 枚举:ACTIVE 状态使用渐变进度,INACTIVE 状态用普通颜色绘制。外层容器根据播放器上报的当前句索引,只对索引对应的那一行调 setState 为 ACTIVE,其他行设为 INACTIVE。如果你打算做逐行滚动,还需要在滚动发生后把新进入可视区的那一行设置为 ACTIVE。这个逻辑不复杂,但容易漏——如果你发现多行歌词里高亮行不对,排查顺序是:先确认当前句索引计算对不对,再确认 ViewHolder 复用时有没有把上一行的 progress 清掉。

6. 二次开发实战:把这个渐变 TextView 改造成你的歌词控件

6.1 扩展方向一:接入真实播放器进度

源码的演示是通过 Handler 模拟进度,二开第一步自然是换成真实播放器数据源。常见播放器框架都会提供 getCurrentPosition() 和 getDuration() 两个方法,你只需要在播放进度回调里取出当前时间,然后对比当前句的 startTime 和 endTime,算出进度,调用 setProgress 即可。

// 假设这是播放器回调,每 200ms 触发一次 private void onPlaybackProgress(long currentTime) { LyricLine currentLine = lyricAdapter.getCurrentLine(currentTime); if (currentLine == null) { lyricTextView.setActive(false); return; } float progress = lyricProgressHelper.calculateProgress( currentTime, currentLine.startTime, currentLine.endTime); lyricTextView.setActive(true); lyricTextView.setProgress(progress); }

注意:播放器回调的间隔不是固定的,不同的播放器引擎差异很大,有的 500ms 才回调一次,渐变会显得一顿一顿。我的做法是:回调到来时更新目标 progress,然后用 ValueAnimator 做 100ms 的补间动画过渡到目标值,视觉上会顺滑很多。这个技巧在歌词逐句切换时尤其重要,因为相邻两句的进度是跳变的,不做平滑处理会显得生硬。

6.2 扩展方向二:多行歌词与滚动联动

做成完整的歌词界面,核心是处理两个问题:当前行高亮切换和当前行居中滚动。RecyclerView 是主流选择,每行一个 item,布局里放一个 LyricTextView 并设置宽高为 match_parent,让歌词文本居中。每次播放进度更新时,找到当前句索引,notify 那一行刷新,同时调用 smoothScrollToPosition 让当前行居中。

这个方案的关键技巧是复用时机:RecyclerView 的 ViewHolder 回收复用会导致上一行残留的高亮状态带到下一行。如果 item 布局用的是同一个 LyricTextView,在 onBindViewHolder 里必须无条件调用 setActive(boolean),哪怕这行不是当前句,否则会出现多行同时高亮的混乱场面。我这里给一个判断当前行的代码习惯:绑定位置等于当前句索引时 setActive(true) 并 setProgress,否则 setActive(false)。注意 setActive(false) 时进度值无所谓,但一定要执行,保证状态被重置。

6.3 扩展方向三:让渐变带一点倾斜和模糊效果

基础水平渐变如果觉得太单调,可以给 LyricTextView 加一个「角度」参数。LinearGradient 的坐标改成斜向:x0=0, y0=0, x1=w, y1=h*0.3f,渐变就会带一个轻微的向下倾斜。视觉上像是「从左下到右上」点亮,很多音乐播放器的歌词就是这么做的。另外还可以在 Paint 上开 setShadowLayer 给文字加一层光晕,让高亮部分有发光感。

但要注意性能边界:setShadowLayer 在 API 28 以下会关闭硬件加速,如果你的项目 minSdk 低于 28,就要慎用。另外,渐变倾斜后,矩阵平移的偏移量计算也要同步按斜率换算,否则高亮扫描线会和文字倾斜方向不一致。我一般建议二开时把倾斜角度做固定值,不要做成可动态调整的 public 方法,因为 Matrix 的复合变换一旦处理不好,扫描效果会非常诡异,而且这种需求本身也很少动态变化。

6.4 一个来自实践的习惯

这套渐变歌词方案我之前在一款音频播放器里用过,当时踩过最大的坑不是绘制,而是歌词切换时 Progress 的跳变处理。那时候我直接在播放器回调里把原生值传进去,结果用户拖进度条时,整行歌词像抽搐一样乱闪,最后老老实实加了一个 80ms 的 ValueAnimator 做钳制,才彻底消掉。从那以后,我每次做这类带进度驱动的自定义 View,都会强制走一遍完整链路:先在布局里用 SeekBar 手拖验证渐变方向,再用固定频率模拟器验证多行切换和复用,最后才接真实播放器数据。这套流程看着多了一步,实际上帮你绕开了至少四五个隐藏很深的坑。希望这篇笔记对你有帮助。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 19:19:42

百度AI人脸属性编辑实战:从接口原理到变老变男变女完整实现

最近一段时间&#xff0c;我被不少人问到一个很有趣的项目&#xff1a;调用百度AI接口&#xff0c;实现把人脸照片变老、变年轻、变男、变女。听起来像是某个美颜App的功能&#xff0c;实际上它背后是一个标准的人脸属性编辑能力&#xff0c;做起来并没有想象中复杂。这篇文章就…

作者头像 李华
网站建设 2026/10/17 19:17:51

Claude Code Hooks 实战:把工具调用日志改到 TaoToken 统一观测

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/17 19:16:28

SAP利润中心转移价格配置:评估方法与变式实操详解

简介&#xff1a;这是聚焦SAP系统利润中心转移价格设定的专业技术资料&#xff0c;面向SAP财务模块实施顾问、成本会计及企业内部管理核算人员。文档从会计学概念切入&#xff0c;说明利润中心转移价格如何通过模拟内部市场交易来考核各利润中心绩效&#xff1b;同时给出SAP中的…

作者头像 李华
网站建设 2026/10/17 19:09:04

UITextView富文本点击与折行实战:链接、高度计算与性能优化

做iOS开发这几年&#xff0c;凡是涉及“用户协议里点链接”“标签可以点击跳转”“长文本要展开收起”这类需求&#xff0c;最后基本都绕回同一个控件&#xff1a;UITextView。每次接到这种需求&#xff0c;很多人的第一反应是“UILabel设置attributedText不也能显示富文本吗”…

作者头像 李华
网站建设 2026/10/17 19:08:41

DIN深度兴趣网络:用注意力机制动态建模CTR预估中的用户行为序列

1. 为什么CTR预估需要DIN&#xff1a;行为序列建模的痛点1.1 平均池化的“平滑陷阱”先从一个很常见的场景说起。某商品平台做CTR预估&#xff0c;用户历史上点击过手机、键盘、羽毛球拍&#xff0c;还买过一袋坚果零食。现在备选池里有一部新手机&#xff0c;模型要不要推给他…

作者头像 李华
网站建设 2026/10/17 19:07:17

基于神经突触共振的大语言模型幻觉抑制技术实践

做大模型落地项目半年多&#xff0c;最折磨我的不是推理速度&#xff0c;而是高置信度幻觉&#xff1a;模型一本正经地编答案&#xff0c;细节、格式全对&#xff0c;唯独事实是错的。为了治它&#xff0c;我研究了一套基于神经突触共振机制的大语言模型幻觉抑制技术&#xff0…

作者头像 李华