最近在我们的项目中,我们实现了一项新功能:在用户选择照片后,系统会首先进行人脸识别。为了提升用户体验,我们决定在进行人脸识别的过程中添加一个弹窗提示。这个弹窗不仅可以让用户了解到识别的进度,还能有效减少用户的等待焦虑,确保他们知道系统正在处理他们的请求。我们希望这个交互能提升整体使用体验,使用户在选择照片后依旧感到流畅和愉快。效果如下:
于是,基于 ImageView,我们实现了一个自定义控件,其核心功能在于绘制扫描效果和噪点。为了创建令人满意的扫描效果,我们选择了绘制一个渐变矩形,并使其在界面上移动。这一过程的实现主要依赖于 Shader 的子类 LinearGradient。
具体来说,LinearGradient 允许我们定义一个从一种颜色渐变到另一种颜色的效果,为矩形提供了丰富的视觉体验。通过调整渐变的起始和结束位置,我们能够创造出一种动态的扫描效果,吸引用户的注意力。同时,为了增加真实感和趣味性,我们还在背景中引入了噪点,这样可以进一步模拟扫描过程中常见的视觉元素,提升用户体验。
private var scanLineOffset: Float = 0.0f
private val scanLineHeight = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, 40f, context.resources.displayMetrics
)
private val scanLinePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.FILL
shader = LinearGradient(
0f, 0f, 0f, scanLineHeight, Color.TRANSPARENT, Color.WHITE, Shader.TileMode.CLAMP
)
}
LinearGradient是线性渐变
public LinearGradient(float x0, float y0, float x1, float y1, @NonNull @ColorInt int[] colors,
@Nullable float[] positions, @NonNull TileMode tile)
总共有7个参数:
-
x0, y0: 渐变起始点的坐标。表示起始颜色的位置
-
x1, y1: 渐变结束点的坐标。表示结束颜色的位置
-
colors: 颜色数组,包含渐变中要使用的颜色
-
positions: 位置数组,包含每个颜色在渐变中的相对位置,值范围在 0 到 1 之间。如果没有提供,Android 将会自动均匀分配
-
tile: 着色模式(Shader.TileMode),定义了如何处理超出渐变范围的区域。可选的值包括:
- Shader.TileMode.CLAMP: 边缘颜色会延续到边
- Shader.TileMode.REPEAT: 渐变会重复
- Shader.TileMode.MIRROR: 渐变会在边缘反转
因为LinearGradient不能动态改变起始点和结束点的位置,修改位置需要重新初始化,所以采用画布平移的方式来实现扫描区域上下移动。
canvas.save()
canvas.translate(0f, scanLineOffset)
canvas.drawRect(0f, -scanLineHeight / 2f, width.toFloat(), scanLineHeight / 2f, scanLinePaint)
canvas.restore()
上下移动的距离计算这边采用的是ValueAnimator动画实现,因为要设置插值器,实现先快后慢的效果,选择了DecelerateInterpolator,最后再根据动画值进行重绘。
插值器(Interpolator)用于控制动画进程的变化。它定义了动画的时间进度与动画间隔(或属性值)之间的关系。Android中有多种内置插值器:
- LinearInterpolator:线性插值器,使得动画以恒定速度进行
- AccelerateInterpolator:加速插值器,动画开始慢,结束时加速
- DecelerateInterpolator:减速插值器,动画开始快,结束时减慢
- AccelerateDecelerateInterpolator:加速减速插值器,开始和结束都慢,中间加速
- BounceInterpolator:弹跳插值器,动画末尾会弹起
可以使用这些插值器来控制动画的流畅程度和风格。
这里顺便说一下估值器(Evaluator)。
估值器用于在动画过程中计算最终的属性值。根据动画的类型,估值器决定如何在开始值和结束值之间进行计算:
- IntEvaluator:用来计算整数类型的属性值
- FloatEvaluator:用来计算浮点类型的属性值
- ArgbEvaluator:用来计算颜色值(ARGB格式),用于渐变效果
估值器在动画的每一帧中根据时间和插值器产生的进度来生成当前的属性值。
插值器控制动画的进程和速度的变化
估值器计算在动画中每一帧需要显示的具体值
private fun startScanAnimation() {
if (height == 0) return
scanLineAnimator = ValueAnimator.ofFloat(0f, height - scanLineHeight / 2f).apply {
duration = 700
interpolator = DecelerateInterpolator()
startDelay = 300
addUpdateListener {
animation ->
scanLineOffset = animation.animatedValue as Float
invalidate()
}
doOnEnd {
scanLineReverseAnimator?.start()
}
start()
}
scanLineReverseAnimator = ValueAnimator.ofFloat(height - scanLineHeight / 2f, 0f).apply {
duration = 700
interpolator = DecelerateInterpolator()
startDelay = 300
addUpdateListener {
animation ->
scanLineOffset = animation.animatedValue as Float
invalidate()
}
doOnEnd {
scanLineAnimator?.start()
}
}
}
然后是绘制噪点,绘制噪点首先需要确定噪点的位置,这边我随机生成了12个噪点,而且通过距离算法控制了每个噪点相距的距离不能少于半径的6倍,这里可以根据自己的要求进行控制,如果只是想要噪点不重叠,就设置成2倍即可。
private fun generateRandomNoises() {
val noiseCount = 12
val maxAttempts = 100
repeat(noiseCount) {
var attempts =