前言
准备写个Demo的时候,发现首页有点空旷,思来想去不知道放点啥内容填充下,后面灵光一闪,能不能做个文字闪烁的效果,放在首页介绍项目。说做就做。
第一步:打开ChatGPT
第二步:搜索"安卓文字闪烁效果用kotlin实现"
第三步:复制粘贴
格式化代码,编译、运行。猿神,启动!!!!
这次移植手术很成功,效果很好!!,效果如下:

效果是不是很nice,好了,做法已经传授给各位了,散了吧。
目前已实现并且上传到MavenCentral,详细用法可以去项目README里面看,项目地址:
觉得可以的给个Star!!!
emmmm,明明我都实现了这个功能了,但为啥总感觉脑袋空空的,做了跟没做似得。不行,还是来分析总结下它的实现思路,不然总感觉哪里怪怪的。
思路
既然是对TextView下手,那么方便起见,我们肯定是简单点直接继承TextView,这里官方建议继承AppCompatTextView,所以,我们定义的FlashTextView初始代码结构就是这样的:
plain
class FlashTextView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = android.R.attr.textViewStyle
) : androidx.appcompat.widget.AppCompatTextView(context, attrs, defStyleAttr) {
}
那在这个基础上,我们来思考下,文字闪烁效果怎么实现?
文字闪烁效果怎么实现?
基于效果图,我们挑选关键的帧看下文本内容的变化

从上图可以看出,随着时间的变化,文字区域有一部分被虚化了。
那这个效果怎么做呢?
在这一步,我瞬间就有了个想法:弄一个矩形区域改在文本上方,随着时间重复移动,起到光效移动的效果。
说干就干。
矩形区域随时间重复移动

如上图所示,我们通过时间的推移,将矩形区域的显示位置移动来达到闪烁的效果。根据这个思路,我们需要定义一个矩形区域Rect,定义一个变量xTranslate记录x轴的移动量,然后通过画笔定义填充的颜色。
plain
class FlashTextView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = android.R.attr.textViewStyle
) : androidx.appcompat.widget.AppCompatTextView(context, attrs, defStyleAttr) {
private val rect = Rect()
private var xTranslate = 0
private val paint = Paint().apply {
setColor("#BBFFFFFF".toColorInt())
style = Paint.Style.FILL
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
//定义这个矩形的高度与文本区域高度一致,宽度为文本区域的宽度的五分之一
rect.set(0, 0, w/5, h)
}
@SuppressLint("DrawAllocation")
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
//每次移动整个宽度的十分之一
xTranslate += measuredWidth/10
//如果超过宽度之后重置为0
if (xTranslate > measuredWidth) {
xTranslate = 0
}
canvas.withTranslation(xTranslate * 1.0f, 0f) {
//绘制文字之后
drawRect(rect, paint)
}
//100毫秒左右重绘一次
postInvalidateDelayed(100)
}
}
每次重绘都会改变x轴的偏移量,通过偏移量的变化矩形的绘制区域,以此完成整个过程的动效,效果如下:

看起来还可以啊!
但是这个矩形区域目前设置的是个纯色的,我们是不是可以把这玩意染成渐变色的?
那怎么染成渐变色呢?我能马上想到的就是:把Rect换成一个Drawable,比如这里可以用:
diff
- ColorDrawable
- ShapeDrawable
- GradientDrawable
- ...
使用Drawable替代矩形区域
这里我选用的 GradientDrawable 来实现一下效果。
plain
rect.set(0, 0, w/7, h)
drawable = GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM, intArrayOf("#66FFFFFF".toColorInt(),"#99FFFFFF".toColorInt(),"#FFFFFFFF".toColorInt()))
drawable?.bounds = rect
然后修改下绘制方式
plain
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
xTranslate += measuredWidth/10
if (xTranslate > measuredWidth) {
xTranslate = 0
}
canvas.withTranslation(xTranslate * 1.0f, 0f) {
//绘制文字之后
drawable?.draw(canvas)
}
postInvalidateDelayed(100)
}
整体代码:
plain
class FlashTextView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = android.R.attr.textViewStyle
) : androidx.appcompat.widget.AppCompatTextView(context, attrs, defStyleAttr) {
private val rect = Rect()
private var xTranslate = 0
private var drawable:GradientDrawable? = null
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
rect.set(0, 0, w/7, h)
drawable = GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM, intArrayOf("#66FFFFFF".toColorInt(),"#BBFFFFFF".toColorInt()))
drawable?.setSize(rect.width(),rect.height())
drawable?.bounds = rect
}
@SuppressLint("DrawAllocation")
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
xTranslate += measuredWidth/10
if (xTranslate > measuredWidth) {
xTranslate = 0
}
canvas.withTranslation(xTranslate * 1.0f, 0f) {
//绘制文字之后
drawable?.draw(canvas)
}
postInvalidateDelayed(100)
}
}
我们来看下效果:

也是很可以的,在原来的基础上面添加了渐变的效果。
我们用自己的想法也实现了个FlashTextView,但是,它存在个问题,就是当我们给TextView设置背景之后,效果就很难看了。和AI提供的方式差别还是挺大的。

AI的实现方式
那AI给我们推荐的代码是怎么实现的呢?它给的代码如下:
plain
class FlashTextView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = android.R.attr.textViewStyle
) : androidx.appcompat.widget.AppCompatTextView(context, attrs, defStyleAttr) {
//矩阵对象
private var gradientMatrix: Matrix? = null
//渐变效果
private var flashGradient: LinearGradient? = null
//平移距离
private var xTranslate = 0
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
if (w > 0 && gradientMatrix == null) {
//保证只初始化一次
flashGradient = LinearGradient(
0f,
0f,
w * 1.0f,
0f,
intArrayOf(Color.BLUE, 0xffffff, Color.BLUE),
null,
Shader.TileMode.CLAMP
)
paint.setShader(flashGradient)
gradientMatrix = Matrix()
}
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
//绘制文字之后
if (gradientMatrix != null) {
xTranslate += measuredWidth / 5
if (xTranslate > 2 * measuredWidth) {//决定文字闪烁的频繁:快慢
xTranslate = -measuredWidth
}
gradientMatrix!!.setTranslate(xTranslate * 1.0f, 0f)
flashGradient!!.setLocalMatrix(gradientMatrix)
postInvalidateDelayed(100)
}
}
}

LinearGradient? setShader? Matrix?
这都是啥?啥?啥?
还好我底子厚实,一百度就知道它们是干甚的了。
那我们就在梳理它的思路之前,先来补充一点点Paint、Shader和Matrix的相关内容先,不然看得的云里雾里。
Paint和Shader
Paint大家应该都不陌生,在自定义View中,它指代绘制的画笔,除了我们常用的设置颜色、风格(Style)外,它还有个setShader(Shader shader) 方法用来设置 Shader。那 Shader 是什么呢?
Shader 这个英文单词很多人没有见过,它的中文叫做「着色器」,也是用于设置绘制颜色的。「着色器」不是 Android 独有的,它是图形领域里一个通用的概念,它和直接设置颜色的区别是,着色器设置的是一个颜色方案,或者说是一套着色规则。当设置了 Shader 之后,Paint 在绘制图形和文字时就不使用 setColor/ARGB() 设置的颜色了,而是使用 Shader 的方案中的颜色。
在 Android 的绘制里使用 Shader ,并不直接用 Shader 这个类,而是用它的几个子类。具体来讲有:
- LinearGradient:线性渐变
- RadialGradient:辐射渐变
- SweepGradient:扫描渐变
- BitmapShader:用 Bitmap 的像素来作为图形或文字的填充
- ComposeShader :混合着色器
这么几个。具体细节大家可以单独去学习了解下,或者关注我,我后面会写个自定义View基础知识的文章。
这里代码使用到了 LinearGradient ,那我就以 LinearGradient 来简单介绍下 Shader 是干甚用的。
首先我们看到它常用的构造方法
plain
public LinearGradient(float x0, float y0, float x1, float y1,
@ColorInt int color0, @ColorInt int color1,
@NonNull TileMode tile) {
this(x0, y0, x1, y1, Color.pack(color0), Color.pack(color1), tile);
}
参数:
- x0 y0 x1 y1:渐变的两个端点的位置,它有两个作用:
- 控制渐变的方向,从 [x0,y0] > [x1,y1]
- 控制渐变的范围,毕竟两个点可以得到渐变的宽或者高
- color0 color1 是端点的颜色
- tile:端点范围之外的着色规则,类型是 TileMode。TileMode 一共有 3 个值可选:CLAMP, MIRROR 和 REPEAT。
- CLAMP 会在端点之外延续端点处的颜色
- MIRROR 是镜像模式
- REPEAT 是重复模式
tile的作用就是超出渐变的范围后,控制后续区域渐变效果。画个图给大家解释下:

编码举个例子:
plain
class ShaderView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = android.R.attr.textViewStyle
) : View(context, attrs, defStyleAttr) {
//渐变效果
private var flashGradient: LinearGradient? = null
private val paint = Paint()
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
flashGradient = LinearGradient(
0f,
0f,
w / 5f * 1.0f,
0f,
intArrayOf(Color.RED, Color.BLUE),
null,
Shader.TileMode.CLAMP
)
paint.setShader(flashGradient)
}
@SuppressLint("DrawAllocation")
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawRect(RectF(0f, 0f, measuredWidth * 1.0f, measuredHeight * 1.0f), paint)
}
}
然后只修改TileMode,不同的TileMode的表现如下:

简单了解了 Shader 之后,我们再来看看 Matrix 矩阵是干嘛的。
Matrix 矩阵
矩阵说起来比较麻烦,全是掉头发的数学知识,作为一个纯正的开发,我自然而然的不会这些知识点。
但是,我不会说不代表我不会用,我们一般将矩阵用在图形变换上,常见的一些操作有:
- 平移
- 缩放
- 翻转
- 旋转
详细说起来也好麻烦,大伙想深入研究的可以自己去查下。这里我简单举下例子。
比如平移操作:
plain
fun setTransition(value:Int) {
transition = value
matrix.setTranslate(transition / 5f * 1.0f, 0f)
flashGradient?.setLocalMatrix(matrix)
invalidate()
}

从结果导向,我们的图像都向右偏移了一段距离,空出来的地方,在不同的模式下表现不同:
- CLAMP模式下,会以端点的颜色延伸,所以补充的红色
- MIRROR模式下,还是镜像的方式反向延伸
- REPEAT模式下,也是反向重复延伸
还举个旋转的操作:
plain
fun setRotate(value:Float) {
matrix.setRotate(value)
flashGradient?.setLocalMatrix(matrix)
invalidate()
}

我们的图像倾斜了一定角度,看起来怪怪的,那为啥是这样的效果呢?

这是因为我们旋转是针对整个图像而言的,比如上图。
然后在显示区域内显示特定区域的内容,所以看起来就很奇怪了,但是理解之后就不觉得奇怪了。
思路
综合上面所有的知识点,我们再来看看AI提供的代码
plain
class FlashTextView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = android.R.attr.textViewStyle
) : androidx.appcompat.widget.AppCompatTextView(context, attrs, defStyleAttr) {
//矩阵对象
private var gradientMatrix: Matrix? = null
//渐变效果
private var flashGradient: LinearGradient? = null
//平移距离
private var xTranslate = 0
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
if (w > 0 && gradientMatrix == null) {
//保证只初始化一次
flashGradient = LinearGradient(
0f,
0f,
w * 1.0f,
0f,
intArrayOf(Color.BLUE, 0xffffff, Color.BLUE),
null,
Shader.TileMode.CLAMP
)
paint.setShader(flashGradient)
gradientMatrix = Matrix()
}
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
//绘制文字之后
if (gradientMatrix != null) {
xTranslate += measuredWidth / 5
if (xTranslate > 2 * measuredWidth) {//决定文字闪烁的频繁:快慢
xTranslate = -measuredWidth
}
gradientMatrix!!.setTranslate(xTranslate * 1.0f, 0f)
flashGradient!!.setLocalMatrix(gradientMatrix)
postInvalidateDelayed(100)
}
}
}
那整体思路就特别清晰了。
首先给TextView的画笔设置了一个 [Color.BLUE, 0xffffff, Color.BLUE] 两端蓝色、中间白色的 LinearGradient,用中间的白色达到闪烁的效果。并且设置的TileMode是CLAMP,了解过上面知识点的就知道,它会两端的颜色延伸,所以保证除了中间的白色闪光效果,其他的文字颜色一致。
那下一步就是让白色闪光点动起来,那这里就是用的Shader的setLocalMatrix方法来动态修改矩阵效果,以达到闪光点动起来的效果。
而且它只是修改了文字的着色效果,所以背景啥的没影响。
存在的问题和思考
AI生成的代码很好用,但是存在一个问题,就是此时给TextView设置的文本颜色是无效的,我们应该让它适应用户设置的颜色。
这里简单修改下:
plain
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
if (w > 0 && gradientMatrix == null) {
//保证只初始化一次
flashGradient = LinearGradient(
0f,
0f,
w * 1.0f,
0f,
intArrayOf(textColors.defaultColor, 0xffffff, textColors.defaultColor),
null,
Shader.TileMode.CLAMP
)
paint.setShader(flashGradient)
gradientMatrix = Matrix()
}
}
这样就是直接使用的用户设置的文本颜色了。
但是如果直接设置的白色的话,就会看不出效果,所以我们可以使用自定义属性的方式来让用户自定义闪光的颜色以及光效范围。这里就不贴代码了。大家可以自己基于这个代码自己去扩展一下,也可以给我提个issues我来补充,我也会慢点更新这个组件的内容。