让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

相关推荐
fundroid31 分钟前
Swift 进军 Android,Kotlin 该如何应对?
android·ios
前端世界34 分钟前
鸿蒙系统安全机制全解:安全启动 + 沙箱 + 动态权限实战落地指南
android·安全·harmonyos
_一条咸鱼_3 小时前
Vulkan入门教程:源码级解析
android·面试·android jetpack
嘉小华3 小时前
ThreadLocal 详解
android
wkj0014 小时前
php 如何通过mysqli操作数据库?
android·数据库·php
kymjs张涛5 小时前
零一开源|前沿技术周报 #7
android·前端·ios
wuwu_q7 小时前
RK3566/RK3568 Android11 修改selinux模式
android·rk3568
_一条咸鱼_8 小时前
Android Runtime内存共享与访问控制原理剖析(71)
android·面试·android jetpack
嘉小华8 小时前
第三章:焦点分发全链路源码解析
android
嘉小华8 小时前
Android 协程全景式深度解析:第六章 高阶并发模式
android