让PAG动画在富文本中动起来

原来来自我的博客让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

相关推荐
撩得Android一次心动11 小时前
Jetpack Compose 知识点整理2【个人用】
android·笔记·学习·kotlin·compose
是真的小外套12 小时前
2024年软考中级信息安全工程师|选择题考点+案例分析真题解析(含答案)
android·网络
用户20187928316714 小时前
一文讲透 Android Native Heap 监控原理:一间"只进不出"的仓库,和总部派来的侦探
android
Ehtan_Zheng14 小时前
Kotlin 伴生块:Android 项目怎么用,迁移时要注意什么
kotlin
JMchen14 小时前
【Jetpack 内核实战 02】ViewModel:生命周期管理与数据持久化核心实战
kotlin·android studio·android jetpack
YB137515 小时前
Jetpack Compose 动画animate*AsState
android
高晶15 小时前
Android 通讯录应用开发实战:从零搭建 SQLite 数据持久化完整项目
android·设计模式·前端框架
隐擎fox15 小时前
深入 Android 移动端风控与协议层对抗:内核级虚拟环境检测、网络堆栈特征伪装与住宅 IP 协同实战
android·网络·python·网络协议·tcp/ip
换元不配限17 小时前
从 AsyncTask 到现代并发:Android 异步方案的演进与替代实践
android·asynctask·异步编程·方案对比·asynctask替代方案