让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

相关推荐
2601_9637713715 小时前
Offloading WP-Cron and Securing Ticket Webhooks on Enterprise IT Sites
android
花燃柳卧16 小时前
跨平台路由组件工程源码补充上传
android·flutter·kotlin
apihz16 小时前
随机驾考题目(C 照科一 / 科四 2000+ 题)免费API调用教程
android·java·c语言·开发语言·网络协议·tcp/ip
宸翰17 小时前
uni-app 设置 Android 底部虚拟导航栏背景色
android·前端·uni-app
TechNomad17 小时前
Kotlin类对象与接口详解
android·kotlin
Ws_18 小时前
Kotlin Multiplatform day2
kotlin
QiLinkOS1 天前
QiLink OS的失败数据共享平台的运作模式是否适用于所有行业?
android·开发语言·人工智能·算法·重构·开源
杉氧1 天前
Framework 补完计划 (2):BufferQueue 与 SurfaceFlinger —— 像素的跨进程“接力赛”
android·架构·android jetpack
牢七1 天前
RCE?复现成功
android
一化十1 天前
Android16 自定义全局手势 任意界面依次点击屏幕四个角返回Home界面
android