今天咱们就聊聊,怎么让Android里的渐变背景"动"起来。
静态渐变打底:GradientDrawable先入门
在聊动态之前,得先知道静态渐变怎么来的。
Android系统提供了一个叫GradientDrawable的东西,可以理解为"渐变画刷"。你可以用XML定义一个渐变:
<shapexmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:type="linear"
android:angle="45"
android:startColor="#FF5722"
android:centerColor="#4CAF50"
android:endColor="#2196F3"/>
</shape>
或者直接在代码里搞:
val gradient = GradientDrawable(
GradientDrawable.Orientation.LEFT_RIGHT,
intArrayOf(Color.RED, Color.GREEN, Color.BLUE)
)
view.background = gradient
注意,XML默认只支持双色渐变,但代码里setColors()可以塞三个甚至更多颜色进去。这是很多开发者第一次用渐变时踩的坑——XML里填了三个颜色死活不生效,换成代码立刻好。
静态渐变好用,但它像一张画好的画,挂在墙上就不动了。要让它流动起来,得请出下一个角色。
方案一:属性动画硬刚——ValueAnimator变色术
这是最直接的思路:用ValueAnimator按时间不断改变渐变的颜色数组,然后每次更新都刷新背景。
核心逻辑长这样:
val animator = ValueAnimator.ofFloat(0f, 1f)
animator.duration = 3000
animator.repeatCount = ValueAnimator.INFINITE
animator.repeatMode = ValueAnimator.REVERSE
animator.addUpdateListener { animation ->
val fraction = animation.animatedFraction
// 在两套颜色之间做线性插值
val blendedColors = intArrayOf(
blendColor(Color.RED, Color.BLUE, fraction),
blendColor(Color.GREEN, Color.YELLOW, fraction),
blendColor(Color.BLUE, Color.RED, fraction)
)
gradientDrawable.colors = blendedColors
}
animator.start()
至于blendColor怎么写?其实就是把两个颜色的ARGB四个通道分别做线性插值。注意要处理颜色带alpha通道的情况,不能简单地当int做加减法,否则颜色过渡会突然跳变。
这种方案简单粗暴,适合拿来做背景色平滑过渡、按钮hover效果等。但有个小问题——它每次回调都在UI线程上改颜色,如果渐变刷新太频繁或者View树太深,可能掉帧。
方案二:自定义View + Shader —— 让渐变"流动"起来
上面的ValueAnimator方案只是在换颜色,本质上还是静态渐变,只不过换得快而已。如果你想要那种"颜色在背景里从左往右流动"的感觉,就得动Shader的位置。
思路是自定义一个View,在onDraw()里用LinearGradient做画笔,然后通过动画不断改变Shader的平移偏移量:
classGradientFlowView@JvmOverloadsconstructor(
context: Context, attrs: AttributeSet? = null
) : View(context, attrs) {
privateval paint = Paint()
privatevar offset = 0f
privateval colors = intArrayOf(Color.RED, Color.GREEN, Color.BLUE, Color.RED)
privateval animator = ValueAnimator.ofFloat(0f, 1f).apply {
duration = 2000
repeatCount = ValueAnimator.INFINITE
addUpdateListener {
offset = it.animatedValue asFloat
invalidate()
}
start()
}
overridefunonDraw(canvas: Canvas) {
super.onDraw(canvas)
val shader = LinearGradient(
-width + offset * width, 0f,
offset * width * 2, 0f,
colors, null, Shader.TileMode.REPEAT
)
paint.shader = shader
canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), paint)
}
}
关键在于最后一行的TileMode.REPEAT——颜色数组首尾颜色相同(Red ~ Red),配合Shader平移偏移量的动画,视觉上就是无限流动的渐变。这种方案性能比频繁setColors()更好,因为Shader是GPU加速的。
方案三:Jetpack Compose时代——animateColor与Brush
如果你用的是Compose,做动态渐变简直像开了外挂。
最简单的背景色动画,animateColorAsState一行搞定:
var isExpanded by remember { mutableStateOf(false) }
val bgColor by animateColorAsState(
targetValue = if (isExpanded) Color.Blue else Color.Red,
animationSpec = tween(durationMillis = 1000)
)
Box(modifier = Modifier.background(bgColor))
想要流动渐变?Compose的Brush家族登场——Brush.linearGradient()、Brush.sweepGradient()、Brush.radialGradient(),搭配rememberInfiniteTransition就是呼吸式渐变:
val infiniteTransition = rememberInfiniteTransition()
val animatedColor by infiniteTransition.animateColor(
initialValue = Color(0xFFFF5722),
targetValue = Color(0xFF2196F3),
animationSpec = infiniteRepeatable(
animation = tween(1500),
repeatMode = RepeatMode.Reverse
)
)
Box(modifier = Modifier.background(
Brush.linearGradient(listOf(animatedColor, Color.White))
))
这套组合拳下来,配合Compose原生的drawWithCache、graphicsLayer等修饰符,你能做出任何你脑海中想象的渐变动效。
容易踩的几个坑
做了这么多年渐变动画,有几个坑值得提前知道。
第一个坑:颜色跳变。 如果你做动画时用setColors()直接切换两套完全不搭的颜色,中间帧可能会经过一段"脏色"。比如从红色到蓝色,中间不是紫色而是诡异的灰色——这是因为ARGB四个通道独立插值的结果。解决方法是手动控制过渡路径,或者用HSV色域做插值,效果会自然很多。
第二个坑:内存抖动。 很多人在addUpdateListener里每次回调都new一个新的IntArray,3秒的动画按60fps算就是180次内存分配。记得把颜色数组提前分配好,回调里直接在同一个数组上改值。
第三个坑:TileMode选错。 做流动渐变时,TileMode有三种——CLAMP(边缘固定)、REPEAT(平铺重复)、MIRROR(镜像重复)。很多人上来就用CLAMP,结果颜色流到一半就停了。流动渐变的正确姿势是REPEAT,并且颜色数组首尾颜色要一致,不然会有一条明显的"接缝"。
真实场景怎么选?
别光看代码,看看现实里大家都怎么用。
音乐播放器是动态渐变的重度用户——专辑封面取主色调,然后背景用这个颜色做呼吸渐变。实现思路是先用Palette库从封面图片提取主色,再把主色和它的亮色/暗色变体丢进ValueAnimator做循环过渡。网易云、Spotify都是这么干的。
启动页和登录页也常用。这种场景一般是静态渐变打底,只在页面进入时播一次欢迎动画——背景从透明渐变成主题色。用Compose的AnimatedVisibility或者View体系的ObjectAnimator.ofFloat(view, "alpha", 0f, 1f)就能搞定。
还有一种骚操作是"跟手渐变"——手指滑动时,渐变的角度或颜色跟随偏移量实时变化。这需要在onTouchEvent里捕获手指位置,手动更新Shader参数然后invalidate()。比静态渐变高级,但代码量也不小,量力而行。
到底用哪种?
简单场景(按钮hover、背景颜色切换): GradientDrawable+ValueAnimator最省事炫酷场景(启动页、音乐播放器、主题皮肤):自定义View + LinearGradient+ Shader平移,性能最好Compose项目:直接用 animateColorAsState和Brush,代码量最少,效果还不打折
记住一个原则:动态渐变虽然好看,但别滥用。一整屏都在流动的背景不仅费电,还容易让用户头晕。点到为止的流动感,才是高级感。
夜雨聆风