SVGA
SVGAPlayer-Android
https://github.com/yyued/SVGAPlayer-Android
SVGA 预览:http://svga.io/svga-preview.html
支持的特性
assets 加载
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() {
}
});
}
网络下载
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 支持
<?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>
动态图像或文本
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
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 不会管理缓存,需要自己管理,否则会出现警告:
SVGAParser can not handle cache before install HttpResponseCache. see https://github.com/yyued/SVGAPlayer-Android#cache
Setup HttpResponseCache
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
<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
- 根据宽等比例缩放高来适配
<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 中解析和重新播放动画,基本能解决问题。
需要注意的是,复用问题,会导致混乱
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
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