原来来自我的博客让PAG动画在富文本中动起来。相关代码可参考:PagDrawable。
我也是最近才接触到了PAG动画,PAG动画就是直播间送礼物时,礼物特效播放的那种动画。类似的是Lottie,但是Lottie相比PAG来说,不能做的很复杂,对于复杂动画播放效率不高。 但是这玩意儿它不能放入Spannable富文本中播放。您可能问了,谁会把礼物特效放在富文本中播放啊?对啊,我也想问啊,做礼物特效场景的库,干嘛非得塞到富文本中啊,谁能知道产品脑子里想的是什么啊?

我经过几日研究,发现这也不是不能实现的,只不过又得曲线救国了。
一、PAG是什么?
Portable Animated Graphics 是一套完整的动效工作流解决方案。 目标是降低或消除动效相关的研发成本,能够一键将设计师在 AE(Adobe After Effects)中制作的动效内容导出成素材文件,并快速上线应用于几乎所有的主流平台。
这是其官网的介绍。
1.1 PAG怎么使用?
groovy
implementation 'com.tencent.tav:libpag:libpag:4.4.25'
xml
<org.libpag.PAGImageView
android:id="@+id/pagImageView"
android:layout_width="240dp"
android:layout_height="240dp"
/>
kotlin
pagImageView.path = "assets://live_follow.pag"
pagImageView.setRepeatCount(-1)
pagImageView.play()
运行效果可参考文章开头的动图中位于上方的控件效果。
基本使用是不是很简单?但是很遗憾的是,这些并不能直接在富文本中使用。
二、如何让PAG动画在富文本中动起来?
富文本,在Android中就是Spannable那一套东西,而在富文本中展示图像,就需要ImageSpan。我们的思路就是,让ImageSpan可以对接Pag动画。
kotlin
class PAGSpan(activity: Activity, onUpdate: (() -> Unit)? = null) : ImageSpan(PAGDrawable(activity, onUpdate)) {
companion object {
private const val TAG = "PAGSpan"
}
var path: String?
get() = pagDrawable.path
set(value) {
pagDrawable.path = value
}
val pagDrawable: PAGDrawable
get() = drawable as PAGDrawable
override fun getSize(
paint: Paint,
text: CharSequence?,
start: Int,
end: Int,
fm: Paint.FontMetricsInt?
): Int {
val width = (paint.measureText(text, start, end) + 0.5f).toInt()
pagDrawable.setBounds(0, 0, width, paint.textSize.toInt())
return width
}
}
只要把这个PAGSapn塞入SpannableString就可以展示一个PAG动画了,当然,还需要PAGDrawable的支持。
kotlin
class PAGDrawable(activity: Activity, private val onUpdate: (() -> Unit)? = null) : Drawable(), PAGDrawableManager.OnPAGDrawCallback {
companion object {
private const val TAG = "PAGDrawable"
}
private var activityRef = WeakReference<Activity>(activity)
private var bitmapRef: WeakReference<Bitmap>? = null
var path: String? = null
set(value) {
val oldValue = field
field = value
if (oldValue != null) {
stop()
}
if (value != null) {
start()
} else {
stop()
}
}
private val srcRect by lazy { Rect() }
private val dstRect by lazy { Rect() }
fun start() {
val p = path ?: throw IllegalStateException("set path value before call start")
val activity = activityRef.get() ?: return
PAGDrawableManager.obtain(activity).register(p, this)
}
fun stop() {
val p = path ?: return
val activity = activityRef.get() ?: return
PAGDrawableManager.obtain(activity).unregister(p, this)
}
override fun draw(canvas: Canvas) {
val bitmap = bitmapRef?.get() ?: return
// 获取原始尺寸和目标尺寸
val srcWidth = bitmap.width.toFloat()
val srcHeight = bitmap.height.toFloat()
val dstWidth = bounds.width().toFloat()
val dstHeight = bounds.height().toFloat()
// 计算缩放比例(取宽度和高度比例中较小的)
val scale = min(dstWidth / srcWidth, dstHeight / srcHeight)
// 计算缩放后尺寸
val scaledWidth = srcWidth * scale
val scaledHeight = srcHeight * scale
// 计算居中位置
val left = (dstWidth - scaledWidth) / 2
val top = (dstHeight - scaledHeight) / 2
// 设置源矩形(完整原始图片)
srcRect.set(0, 0, bitmap.width, bitmap.height)
// 创建目标矩形(保持比例并居中)
dstRect.set(
left.toInt(),
top.toInt(),
(left + scaledWidth).toInt(),
(top + scaledHeight).toInt()
)
canvas.drawBitmap(bitmap, srcRect, dstRect, null)
onUpdate?.invoke()
}
override fun onDraw(bitmap: Bitmap) {
bitmapRef = WeakReference(bitmap)
invalidateSelf()
}
override fun setAlpha(alpha: Int) {}
override fun setColorFilter(colorFilter: ColorFilter?) {}
@Deprecated("Deprecated in Java")
override fun getOpacity(): Int {
return PixelFormat.TRANSLUCENT
}
}
在PAGDrawable中,主要是靠PAGDrawableManager
控制动画播放,而PAGDrawableManager中,主要是维护了依托于PAGImageView的刷新回调分发,当创建了一个PAGDrawable时,会根据绑定的pag资源路径,进行创建或者查找一个PAGImageView,并将其放置在当前activity的屏幕窗口之外的位置,让其一直播放,并进行刷新事件的订阅,刷新事
注意,这并不是一个严肃的实现方式,只是一种迫于无奈之下的奇技淫巧,可改进空间可能很大,只是提供一种思路。
源码地址:PagDrawable