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 不支持的情况
- AE 插件不支持,粒子效果
https://github.com/yyued/SVGAPlayer-Android/issues/45 - 对 AE 动画支持有限的效果和类型
- TEXT 不支持
- 复杂动画转换较慢
- 不适合交互的场景
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 图片替换
- SVGA 资源文章
http://svga.io/article.html
SVGA 不能全屏播放,控制动画的宽高?
- 设置 SVGAImageView 的 width 和 height
- 设置 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
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
- 用 DialogFragment 做这种全屏礼物动画时,容易 OOM,改为 addView 方式
- SVGAImageView 需要动态添加,每次用完就 removeView 掉不然容易 OOM