文章

SVGA

SVGA

SVGAPlayer-Android

https://github.com/yyued/SVGAPlayer-Android
SVGA 预览:http://svga.io/svga-preview.html

支持的特性

assets 加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void loadAnimation() {
    SVGAParser parser = new SVGAParser(this);
    String heartbeat_choice_success = "svga/heartbeat_choice_success.svga";
    parser.parse(heartbeat_choice_success, new SVGAParser.ParseCompletion() {
        @Override
        public void onComplete(@NotNull SVGAVideoEntity videoItem) {
            animationView.setVideoItem(videoItem);
            animationView.startAnimation();
        }

        @Override
        public void onError() {

        }
    });
}

网络下载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private void loadAnimation() {
    try {
        File cacheDir = new File(this.getCacheDir(), "http");
        HttpResponseCache.install(cacheDir, 1024 * 1024 * 128);
    } catch (IOException e) {
        e.printStackTrace();
    }

    SVGAParser parser = new SVGAParser(this);
    try { // new URL needs try catch.
        parser.parse(new URL("https://github.com/yyued/SVGA-Samples/blob/master/posche.svga?raw=true"), new SVGAParser.ParseCompletion() {
            @Override
            public void onComplete(@NotNull SVGAVideoEntity videoItem) {
                animationView.setVideoItem(videoItem);
                animationView.startAnimation();
            }

            @Override
            public void onError() {

            }
        });
    } catch (MalformedURLException e) {
        e.printStackTrace();
    }
}

Layout 支持

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#000000">

    <com.opensource.svgaplayer.SVGAImageView
        android:layout_height="match_parent"
        android:layout_width="match_parent"
        android:scaleType="fitCenter"
        app:source="svga/angel.svga"
        app:antiAlias="true"/>

</RelativeLayout>

动态图像或文本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
private void loadAnimation() {
    SVGAParser parser = new SVGAParser(this);
    try { // new URL needs try catch.
        String heartbeat_choice_success = "svga/heartbeat_choice_success.svga";
        String url = heartbeat_choice_success;
        URL url2 = new URL("https://github.com/yyued/SVGA-Samples/blob/master/kingset.svga?raw=true");
        parser.parse(url, new SVGAParser.ParseCompletion() {
            @Override
            public void onComplete(@NotNull SVGAVideoEntity videoItem) {

                SVGADynamicEntity dynamicEntity = new SVGADynamicEntity();

                Bitmap avatarBm = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher);
//                    Bitmap avatarBm2 = BitmapFactory.decodeResource(getResources(), R.drawable.svga_replace_avatar);
                
                dynamicEntity.setDynamicImage("https://github.com/PonyCui/resources/blob/master/svga_replace_avatar.png?raw=true", "avatar2"); // Here is the KEY implementation.
//                    dynamicEntity.setDynamicImage(avatarBm2, "avatar1"); // Here is the KEY implementation.

                dynamicEntity.setDynamicImage(avatarBm, "avatar1"); // Here is the KEY implementation.

                TextPaint textPaint = new TextPaint();
                textPaint.setColor(Color.WHITE);
                textPaint.setTextSize(30);
                dynamicEntity.setDynamicText("大圣哥", textPaint, "name1");

                TextPaint textPaint2 = new TextPaint();
                textPaint2.setColor(Color.WHITE);
                textPaint2.setTextSize(30);
                dynamicEntity.setDynamicText("哈哈姐", textPaint2, "name2");

//                    SVGADrawable drawable = new SVGADrawable(videoItem, dynamicEntity);
                animationView.setVideoItem(videoItem, dynamicEntity);

                animationView.startAnimation();

                LogUtil.i(TAG, "onComplete,SVGAVideoEntity3:" + videoItem.toString());
            }

            @Override
            public void onError() {
                LogUtil.e(TAG, "onError3");
            }
        });
    } catch (MalformedURLException e) {
        e.printStackTrace();
        LogUtil.e(TAG, e.getMessage());
    }
}

使用位图替换指定元素

https://github.com/yyued/SVGAPlayer-Android/wiki/Dynamic-Image

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
try {
    parser.parse(new URL("https://github.com/yyued/SVGA-Samples/blob/master/kingset.svga?raw=true"), 
        new SVGAParser.ParseCompletion() {
            @Override
            public void onComplete(@NotNull SVGAVideoEntity videoItem) {
                SVGADynamicEntity dynamicEntity = new SVGADynamicEntity();
                String imageKey = "99";
                dynamicEntity.setDynamicImage("https://github.com/PonyCui/resources/blob/master/svga_replace_avatar.png?raw=true", imageKey); // Here is the KEY implementation.
                SVGADrawable drawable = new SVGADrawable(videoItem, dynamicEntity);
                testView.setImageDrawable(drawable);
                testView.startAnimation();
            }
            @Override
            public void onError() {

            }
        }
    );
} catch (Exception e) {
    System.out.print(true);
}

效果:

在指定元素上绘制文本

设计大佬那里是一张透明的图片,客户端在上面绘制文本
https://github.com/yyued/SVGAPlayer-Android/wiki/Dynamic-Text

在指定元素上绘制富文本

https://github.com/yyued/SVGAPlayer-Android/wiki/Dynamic-Text-Layout

隐藏指定元素

https://github.com/yyued/SVGAPlayer-Android/wiki/Dynamic-Hidden

在指定元素上自由绘制

https://github.com/yyued/SVGAPlayer-Android/wiki/Dynamic-Drawer

注意

SVGA 不支持的情况

  1. AE 插件不支持,粒子效果
    https://github.com/yyued/SVGAPlayer-Android/issues/45
  2. 对 AE 动画支持有限的效果和类型
  3. TEXT 不支持
  4. 复杂动画转换较慢
  5. 不适合交互的场景

cache

SVGAParser 不会管理缓存,需要自己管理,否则会出现警告:

1
SVGAParser can not handle cache before install HttpResponseCache. see https://github.com/yyued/SVGAPlayer-Android#cache

Setup HttpResponseCache

1
2
val cacheDir = File(context.applicationContext.cacheDir, "http")
HttpResponseCache.install(cacheDir, 1024 * 1024 * 128)

svga path shape 动画导致内存持续增长

在某些手机,svga 中有使用 Path 路径绘制的 shape 动画的,svga 会在绘制动画的过程中,动态的生成各种 path 来绘制 shape 动画,60 帧的动画,可能最后会生成几千个参数不同的 path 对象,数量有可能更多,由于硬件加速的实现问题,path 的参数稍有不同,某些手机会为每一个不同 path 绘制分配一块新的内存绘制,从而导致动画一直进行的话,分配的内存会一直持续增长,在 le max 2 这个手机上实验,demo 里面加载会增长到 300M,我们现在是让设计不使用 shape 动画来解决的

希望尽量少用 shape,因为 shape 的绘制确实太耗性能了。

解决:用 png 图片替换

https://github.com/yyued/SVGAPlayer-Android/issues/146

SVGA 不能全屏播放,控制动画的宽高?

  1. 设置 SVGAImageView 的 width 和 height
  2. 设置 SVGAImageView 的 scaleType
1
2
3
4
5
6
<com.opensource.svgaplayer.SVGAImageView
        android:id="@+id/iv_svga"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/transparent"
        android:scaleType="centerCrop" />

如何控制动画的宽高?我有个动画展示的场景是在半屏下播放的
https://github.com/yyued/SVGAPlayer-Android/issues/165

  1. 根据宽等比例缩放高来适配
1
2
3
4
5
6
7
8
9
10
11
12
<android.support.constraint.ConstraintLayout>
    <com.opensource.svgaplayer.SVGAImageView
        android:id="@+id/iv_svga_top"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:scaleType="centerCrop"
        android:visibility="gone"
        app:layout_constraintDimensionRatio="640:1138"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"/>
</android.support.constraint.ConstraintLayout>

SVGAImageView 在 recyclerciew 中使用有问题

上下滑动,导致有些播放不了动画了
https://github.com/yyued/SVGAPlayer-Android/issues/167

方案 1:在 RecyclerView 的 onViewAttachedToWindow() 重新 startAnimation()

我们在项目里也遇到了这个问题,在 RecyclerView 滑回已经展示过的部分 item 时没有重新走入 onBindViewHolder 方法,所以尝试在 onViewAttachedToWindow 中解析和重新播放动画,基本能解决问题。
需要注意的是,复用问题,会导致混乱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
abstract class BaseQuickAdapterSVGA<T, K : BaseViewHolder> @JvmOverloads constructor(@LayoutRes layoutId: Int = 0, data: MutableList<T>?) : BaseQuickAdapter<T, K>(layoutId, data) {

    companion object {
        private const val TAG = "svga"
    }

    override fun onViewAttachedToWindow(holder: K) {
        super.onViewAttachedToWindow(holder)
        val views = getSVGAViews(holder)
        val position = holder.adapterPosition
        val item = getItem(position)
        for (index in views.indices) {
            val view = views[index]
            if (view != null && item != null) {
                val tag = view.tag as? String
                if (svgaTag(position, item, view) == tag) {
                        view.startAnimation()
                    }
                } else {
            }
        }
    }

    abstract fun svgaTag(position: Int, item: T, view: SVGAImageView): String?

    abstract fun getSVGAViews(holder: K): List<SVGAImageView?>
}

方案 2:重写 SVGAImageView 在 onAttachedToWindow 时 startAnimation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
open class CommonSVGAView @JvmOverloads constructor(
        context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : SVGAImageView(context, attrs, defStyleAttr) {

    private var parseCallback: ParseCallback? = null
    private val lifecycleOwner: LifecycleOwner
    private var url: String = ""

    companion object {
        const val TAG = "hacket"
    }

    init {
        setLoop(1)
        if (context !is LifecycleOwner) {
            throw AssertionError("context must be implements LifecycleOwner!")
        }
        lifecycleOwner = context
    }

    fun loop(): CommonSVGAView {
        setLoop(0)
        return this
    }

    fun setLoop(loop: Int): CommonSVGAView {
        loops = loop
        return this
    }

    fun setCallback(callback: ParseCallback?): CommonSVGAView {
        this.parseCallback = callback
        return this
    }

    fun clearCallback() {
        this.parseCallback = null
    }

    fun show(url: String): CommonSVGAView {
        this.url = url
        lifecycleOwner.lifecycleScope.launch {
            logd("parse", "url=$url")
            parseSVGA(url)
        }
        return this
    }

    fun showAsset(assetPath: String): CommonSVGAView {
        this.url = assetPath
        lifecycleOwner.lifecycleScope.launch {
            // logd("parse", "url=$url")
            parseSVGA(assetPath, true)
        }
        return this
    }

    fun stop(): CommonSVGAView {
        if (isAnimating) {
            stopAnimation(true)
        }
        return this
    }


    private suspend fun parseSVGA(url: String, isAssets: Boolean = false) {
        try {
            val s = SystemClock.uptimeMillis()
            val parser = SVGAParser(context.applicationContext)
            val listener = object : SVGAParser.ParseCompletion {

                override fun onError() {
                    logw("onError", "parseSVGA onError,isAssets=$isAssets,url=$url")
                    parseCallback?.onError()
                }

                override fun onComplete(videoItem: SVGAVideoEntity) {
                    logd("onComplete", "parseSVGA onComplete,isAssets=$isAssets,url=$url,cost:"
                            + (SystemClock.uptimeMillis() - s) + "ms")
                    val drawable = SVGADrawable(videoItem,
                            parseCallback?.onPreComplete(videoItem) ?: SVGADynamicEntity())
                    parseCallback?.onComplete(videoItem)
                    setImageDrawable(drawable)
                    tag = url
                    startAnimation()
                    callback = object : SVGACallback {
                        override fun onPause() {}

                        override fun onFinished() {
                            logd("onFinished", "parseSVGA onFinished,isAssets=$isAssets,url=$url")
                            parseCallback?.onFinished()
                        }

                        override fun onRepeat() {}

                        override fun onStep(frame: Int, percentage: Double) {}
                    }
                }
            }
            withContext(Dispatchers.IO) {
                if (isAssets) {
                    logw("parse", "parseSVGA 从Assets加载SVGA,url=$url")
                    parser.decodeFromAssets(url, listener)
                } else {
                    if (url.startsWith("http")) {
                        logw("parse", "parseSVGA 从网络加载SVGA,url=$url")
                        parser.decodeFromURL(URL(url), listener)
                    } else {
                        logd("parse", "parseSVGA 从本地缓存加载SVGA,url=$url")
                        val inputStream = FileInputStream(url)
                        parser.decodeFromInputStream(inputStream, url, listener, true)
                    }
                }
            }
        } catch (e: Exception) {
            logw("parse", "parseSVGA Exception:${e.message},从网络加载url=$url")
            parseCallback?.onError()
            e.printStackTrace()
        }
    }

    override fun onAttachedToWindow() {
        super.onAttachedToWindow()
        val t = tag as? String
        if (t == this.url) {
            if (drawable != null && !isAnimating) {
                logd("onAttachedToWindow", "startAnimation t=$t,url=$url,view=${this}")
                startAnimation()
            } else {
                logw("onAttachedToWindow", "drawable为null或正在isAnimating,isAnimating=$isAnimating,drawable=$drawable,t=$t,url=$url")
            }
        } else {
            logw("onAttachedToWindow", "tag和url不匹配,t=$t,url=$url")
        }
    }


    private fun logd(anchor: String, msg: String) {
        LogUtils.d(TAG, "${anchor(anchor)}$msg")
    }

    private fun logw(anchor: String, msg: String) {
        LogUtils.w(TAG, "${anchor(anchor)}$msg")
    }

    interface ParseCallback {
        fun onError()
        fun onComplete(videoItem: SVGAVideoEntity) {}
        fun onPreComplete(videoItem: SVGAVideoEntity): SVGADynamicEntity = SVGADynamicEntity()
        fun onFinished() {}
    }

    // 水平镜像
    fun onMirrorHorizontally() {
        scaleY = 1f
        scaleX = -1f
        requestLayout()
    }

}

SVGA 设计规范

https://github.com/yyued/SVGAPlayer-Android/issues/109

https://github.com/yyued/SVGAPlayer-Android/issues/71

我估计是你的设计师偷懒使用序列帧导出给你了,这是绝对禁止的。

SVGA OOM

  1. 用 DialogFragment 做这种全屏礼物动画时,容易 OOM,改为 addView 方式
  2. SVGAImageView 需要动态添加,每次用完就 removeView 掉不然容易 OOM
本文由作者按照 CC BY 4.0 进行授权