Android 残影数字属性动画效果,以及叠加梯形基座自定义View实现详解,附上GitHub代码资源(Kotlin)。
项目上要实现一个残影叠加效果的评分动画效果,常规数字显示较轻的残影动效,逢10的倍数残影效果加重,并在结束的时候带有抖动效果,分数底座跟随评分等级切换颜色,先看下实现效果GIF。
初始版本实现(ScoreAnimView):

优化后的版本实现(GhostingTextView):

一、实现原理详解(初始版本)
首先是得分动画,叠加了缩放、alpha透明度渐变、从下往上Y轴方向的偏移,以及加重动效时候阴影在X轴方向的偏移和描边stroke粗细大小的变化。
1)从底部冒出动画效果
略过缩放和alpha透明度渐变常规操作,如果只是配置缩放和透明度渐变则字体是从布局中心点开始做缩放,不符合设计要求的效果,需要叠加从下往上Y轴方向的偏移,比如translationY属性从100->0就是从下往上冒出来的效果。在不同的屏幕尺寸下,不同大小的文本SCORE_TRANSLATION_Y和X_TRANSLATION_Y偏移量参数要各自适配,这样X和数字文本从下往上缩放显示的时候底部可以在同一水平线上。
val transYAnimator: ObjectAnimator = ObjectAnimator.ofFloat(scoreNum, "translationY", SCORE_TRANSLATION_Y, 0f)
2)残影叠加动画效果
残影效果则是在布局里面使用两个相同的控件叠加,一前一后,前面控件的做常规的动画,后面控件的延迟50ms执行动画,这样层叠错开显示形成残影效果。逢10的时候做描边stroke粗细大小的渐变,以及阴影在X轴方向偏移量的渐变,达到加重突出的动画效果。
前后两个控件分别用独立的属性动画执行,使用AnimatorSet的playTogether同步触发控件动画的执行。
//上层字体动画 mPackAnimSet = AnimatorSet() mPackAnimSet?.playTogether(mScoreAnimSet, mScorePlusAnimSet) //底层残影叠加动画 mPackAnimSetShadow = AnimatorSet() mPackAnimSetShadow?.playTogether(mScoreAnimShadowSet, mScorePlusShadowAnimSet) //底层残影叠加加重动画 mPackAnimSpecSetShadow = AnimatorSet() mPackAnimSpecSetShadow?.playTogether(mScoreAnimShadowSpecSet, mScorePlusShadowAnimSet)
而阴影在X轴的偏移量没有直接的属性函数可以调用,需要间接通过setShadowLayer函数来配置实现:
fun setShadowDx(dx: Float){ super.setShadowLayer(shadowRadius, dx, shadowDy, shadowColor) }
镂空的数字文本则通过加载字体库,并自定义画笔的style和strokeWidth参数实现,这样可以自己配置strokeWidth属性的渐变。
//加载字体库 val fontScore = Typeface.createFromAsset(context.getAssets(), "Montserrat-ExtraBoldItalic.ttf") setTypeface(fontScore) //绘制镂空字体 override fun onDraw(canvas: Canvas?) { paint.style = Paint.Style.STROKE paint.color = mStrokeColor paint.strokeWidth = mStrokeWidth super.onDraw(canvas) }
3)梯形底座
实际是构造三个三角形Path区域,通过LinearGradient配置渐变到三角形三分二高度为透明,实现绘制叠加梯形的的底座效果。具体实现见代码。
二、实现原理详解(优化版本)
首先是使用属性动画从0.3到1.0的参数缩放,在缩放的过程中改变文本尺寸大小:
var scaleAnimator = ObjectAnimator.ofFloat(this, "scaleShift", 0.3f, 1.0f)
fun setScaleShift(ratio: Float) { mIsAnimEnd = false mScaleRatio = ratio mAnimTextSize = TEXT_SIZE * ratio invalidate() }
使用for循环逐个减小文本大小尺寸,配合逐级递减的Alpha透明度绘制残影效果:
for (i in GHOSTING_NUMBER downTo 0) { var itemAlpha = 255 - 255 / GHOSTING_NUMBER * i if (itemAlpha < 0) { itemAlpha = 0 } drawGhosting(mAnimTextSize - i * 20, itemAlpha, canvas) }
而加重的残影效果通过strokeWidth的属性渐变来实现:
val strokeAnimator: ObjectAnimator = ObjectAnimator.ofFloat( this, "strokeWidth", VIEW_STROKE_WIDTH, VIEW_STROKE_WIDTH * 5, VIEW_STROKE_WIDTH )
fun setStrokeWidth(strokeWidth: Float) { mStrokeWidth = strokeWidth }
逢10倍数数字抖动的动效通过translationX和translationY的变化来实现:
val translateXAnim: ObjectAnimator = ObjectAnimator.ofFloat(this, "translationX", 8f, 0f, 8f, 0f,8f, 0f,8f, 0f,8f, 0f, 8f, 0f, 8f, 0f,8f, 0f,8f, 0f,8f, 0f) val translateYAnim: ObjectAnimator = ObjectAnimator.ofFloat(this, "translationY", 4f, 0f, 4f, 0f, 4f, 0f,4f, 0f,4f, 0f, 4f, 0f, 4f, 0f, 4f, 0f,4f, 0f,4f, 0f)
这边需要注意的点:在布局中使用GhostingTextView需要配置minWidth和minHeight属性,因为重写了onDraw,也没写死布局尺寸,不配置会出现绘制显示的时候显示不全的问题。
另外,目前使用的字体在显示数字7的时候,右上角被裁切,显示不全,这个通过添加空格解决,为了不让空格占的位置过大,通过SpannableString设置较小的字体。
if (mScore % 10 == 7) { //避免右上角被裁剪,使用小尺寸空格填充 var str7 = " ${mScore} " var spannable7 = SpannableString(str7) spannable7.setSpan(mScoreTailAbsoluteSizeSpan, 0, 1, Spannable.SPAN_INCLUSIVE_INCLUSIVE) spannable7.setSpan(mScoreAbsoluteSizeSpan, 1, str7.length - 1, Spannable.SPAN_INCLUSIVE_INCLUSIVE) spannable7.setSpan(mScoreTailAbsoluteSizeSpan, str7.length - 1, str7.length, Spannable.SPAN_INCLUSIVE_INCLUSIVE) mGhostingTv.setScore(spannable7) }
三、GitHub仓库链接地址
https://github.com/wenguan0927/GhostingAnimView
扩展阅读:
转载请注明出处:陈文管的博客 – Android 残影数字动画实现详解
扫码或搜索:文呓

微信公众号 扫一扫关注