说到动画,最开始想到的是帧动画,补间动画,以及属性动画,以上动画其实已经实现了应用中的大多数效果。
但是很多对用户交互要求比较高的应用仅依靠原生来搞已经没法满足要求了。
当然 MP4,GIF也是一种方案,但是哪款应用不去考虑性能以及内存呢,所以这篇文章介绍一下 Lottie,WebP,以及SVGA的基本使用,对于原生开发来说,上面的三种动画效果不需要我们去考虑,这里设计小姐姐就可以去设计,我们只是用一个容器去显示即可。
个人能力有限,不过多去介绍三者的原理以及优缺点
这里使用的 .json 的文件,当然根据需求我们可能会放一些对应的图片到里面去(需要与json文件里的参数名字对应)
1.添加依赖
com.airbnb.android:lottie:4.2.2
2.使用方式
2.1在布局文件里面 .xml
lottie_autoPlay - 是否自动播放
lottie_loop - 是否循环播放
lottie_fileName - 需要播放的 json 文件位置,需要放在 assets 文件夹下
2.2代码里面
lav_j.setAnimation("lottie/lot_ld.json") //动态添加播放文件
lav_j.playAnimation() //开始播放
lav_j.isAnimating() //是否正在播放动画
lav_j.cancelAnimation() //取消播放
lav_j.setProgress() //设置到第几帧
1.添加依赖
com.facebook.fresco:webpsupport:2.6.0
com.facebook.fresco:animated-webp:2.6.0
2.使用
布局文件里面
代码里面动态赋值
val controller: DraweeController = Fresco.newDraweeControllerBuilder()
.setUri(url)
.setAutoPlayAnimations(true)
.build()
iv.controller = controller
因为SVGA可以播放比较高质量的动画,比如直播间送礼特效的动画,所以SVGA的初始化相对以上两种稍微繁琐一点,类似于送礼动画不可能每次都去下载解析,所以SVGA具备缓存功能
1.导入依赖
com.github.yyued:SVGAPlayer-Android:2.6.1
2.初始化SVGA这里建议在 Application 里面初始化,尽量不要使用时在初始化,因为播放时还是比较消耗性能的
SVGAHelper.init(BaseApplication.context)
这里简单说一下使用,后面直接贴源码
2.1清单文件中
2.2代码中
SVGAHelper.startParse("svga/zbj_lw_dh.svga", object : SVGAParser.ParseCompletion {
override fun onComplete(videoItem: SVGAVideoEntity) {
mViewDataBinding.ivLiveGift.setVideoItem(videoItem)
mViewDataBinding.ivLiveGift.startAnimation()
}
override fun onError() {
}
})
可以看到 startParse 传入的是一个本地路径,这里也可以传入链接,下面源码中有贴出
通过回调得到内部封装的一个数据类,赋予我们定义的控件即可
SVGA还支持替换内部图片,比如一个动画需要一个头像,但是这个头像还不能写死,要动态显示,多个动画肯定是不现实的,这时就需要替换
替换时需要注意与动画里面定义好的参数名称一致,这里要与做动画的UI小姐姐对应好
回调的时候我们已经拿到了封装好的内部数据类,我们只需要对这个数据二次操作在给控件赋值即可
val dynamicEntity = SVGADynamicEntity()
dynamicEntity.setDynamicImage(handList[0], "zuoxia")
if (mVideoItem != null) {
val drawable = SVGADrawable(mVideoItem!!, dynamicEntity)
mViewDataBinding.ivSvga.setImageDrawable(drawable)
mViewDataBinding.ivSvga.loops = 1
mViewDataBinding.ivSvga.startAnimation()
}
可以看到通过新的数据类
setDynamicImage 重新赋值即可,看源码得知前面是需要替换的图片 bitmap 或者 url
后面则是对应的参数名
接下来在创建一个新的SVGADrawable ,重新赋值即可
SVGAHelper 源码:
@SuppressLint("StaticFieldLeak")
/**
*
* lru内存缓冲池
*
* 资源解压缓存目录 File(context.cacheDir.absolutePath + "/" + cacheKey + "/")
*
* 网络请求缓存目录 FileUtils.getCachePath(mContext), "svgaCache") 有效期60天
*
*
* 本地资源asset解析使用rxjava线程池 网络资源使用默认okhttp的线程池
*
*
* @author zhangzhen
* *
* @data 2017/11/1
*/
object SVGAHelper {
val logger = ULog.getLogger("SVGAHelper")
private lateinit var mContext: Context
//一定要初始化 不然后续都报错
fun init(context: Context) {
mContext = context
resetDownloader()
}
private val lruCache: LruCache by lazy {
val size = getMaxCacheSize(mContext)
object : LruCache(size) {
override fun sizeOf(key: String, value: SVGAVideoEntity): Int {
//由于对象SVGAVideoEntity内存占用大头是images 这里只计算images 内存占用总和
return getSize(value.frames)
}
}
}
private val parser: SVGAParser by lazy {
SVGAParser(context = mContext)
}
// private val parser: SVGAParser by lazy {
// shareParser()
//}
private fun get(key: String): SVGAVideoEntity? {
logger.info("当前的lruCache 情况:${lruCache.size()}/${lruCache.maxSize()} 占比${lruCache.size() * 100 / lruCache.maxSize()}%")
return lruCache.get(key)
}
private fun put(key: String, item: SVGAVideoEntity) {
lruCache.put(key, item)
}
/**
* 传入的assets
*/
fun parse(assetsName: String, callback: SVGAParser.ParseCompletion) {
// val key = cacheKey("file:///assets/" + assetsName)
// val item = get(key)
// if (item != null) {
// Log.e("读取缓存", "====")
logger.info("有缓存 直接读取缓存$assetsName")
// callback.onComplete(item)
// } else {
// Log.e("直接加载", "====")
parser.decodeFromAssets(assetsName, object : SVGAParser.ParseCompletion {
override fun onComplete(videoItem: SVGAVideoEntity) {
callback.onComplete(videoItem)
// put(key, videoItem) //将结果存进缓存
}
override fun onError() {
callback.onError()
}
})
// Observable.create {
// var svg: SVGAVideoEntity? = null
// mContext.assets.open(assetsName)?.let {
// svg = parse(it, key)
// }
// if (svg != null)
// it.onNext(svg!!)
// }.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe({
// callback.onComplete(it)
// put(key, it) //将结果存进缓存
logger.info("成功获取 当前订阅的的线程:" + Thread.currentThread().name)
// }, {
// callback.onError()
// it.printStackTrace()
// }, {
//
// })
// }
}
/**
* 这里没办法 私有方法只能反射去获取 再执行
* 库自带的方法每次都创建线程 太low 这里使用rxjava线程池
*/
private fun parse(inputStream: InputStream, cacheKey: String): SVGAVideoEntity? {
val cls = parser!!.javaClass
//获得类的私有方法
val method = cls.getDeclaredMethod("parse", InputStream::class.java, String::class.java)
method.isAccessible = true //没有设置就会报错
//调用该方法
val videoItem = method.invoke(parser, inputStream, cacheKey) as? SVGAVideoEntity
// val videoItem= parser.parse(inputStream,cacheKey)
return videoItem
}
var svgaplay: Boolean = true
/**
* 传入的url 网络请求的解析
*/
fun parse(url: URL, callback: SVGAParser.ParseCompletion) {
svgaplay = true
// val key = cacheKey(url)
// val item = get(key)
// if (item != null) {
// Log.e("读取缓存", "====")
logger.info("有缓存 直接读取缓存")
// callback.onComplete(item)
// } else {
// Log.e("直接加载", "====")
parser.decodeFromURL(url, object : SVGAParser.ParseCompletion {
override fun onComplete(videoItem: SVGAVideoEntity) {
if (svgaplay) {
// put(key, videoItem) //将结果存进缓存
callback.onComplete(videoItem)
svgaplay = false
}
}
override fun onError() {
callback.onError()
}
})
}
/**
* 传入的url 网络请求的解析 不使用内存缓存 (对于同一个svga同时多个view播放时 不能使用缓存 否则只会有一个播放)
*/
fun parseNoCache(url: String, callback: SVGAParser.ParseCompletion) {
parser.parse(URL(url), object : SVGAParser.ParseCompletion {
override fun onComplete(videoItem: SVGAVideoEntity) {
callback.onComplete(videoItem)
}
override fun onError() {
callback.onError()
}
})
}
//总的调用入口
fun startParse(url: String, callback: SVGAParser.ParseCompletion) {
if (url.startsWith("http://") || url.startsWith("https://")) {
parse(URL(url), callback)
} else {
parse(url, callback)
}
}
private fun cacheKey(str: String): String {
val messageDigest = MessageDigest.getInstance("MD5")
messageDigest.update(str.toByteArray(charset("UTF-8")))
val digest = messageDigest.digest()
val sb = StringBuffer()
for (b in digest) {
sb.append(String.format("%02x", b))
}
return sb.toString()
}
private fun cacheKey(url: URL): String {
return cacheKey(url.toString())
}
/**
* 清空缓冲池
*/
fun clear() {
lruCache.evictAll()
}
/**
* @param mContext
* *
* @return 设置最大的缓存内存值 这里设置成1/9 最大内存
*/
fun getMaxCacheSize(mContext: Context): Int {
val mActivityManager =
mContext.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val maxMemory = Math.min(mActivityManager.memoryClass * ByteConstants.MB, Integer.MAX_VALUE)
if (maxMemory < 32 * ByteConstants.MB) {
return 4 * ByteConstants.MB
} else if (maxMemory < 64 * ByteConstants.MB) {
return 6 * ByteConstants.MB
} else {
// We don't want to use more ashmem on Gingerbread for now, since it doesn't respond well to
// native memory pressure (doesn't throw exceptions, crashes app, crashes phone)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
return 8 * ByteConstants.MB
} else {
return maxMemory / 6
}
}
}
/**
* @param images
* *
* @return 返回该集合所占用内存总和
*/
fun getMapsSize(images: HashMap): Int {
val entrySet = images.entries
var size = 0
for ((_, value) in entrySet) {
size += getBitmapSize(value)
}
return size
}
/**
* @param images
* *
* @return 返回该集合所占用内存总和
*/
var size = 0
fun getSize(bitmapSize: Int): Int {
size = +bitmapSize
Log.e("当前的lruCache 情况", "==" + size)
return size * 100
}
//计算bitmap大小
private fun getBitmapSize(value: Bitmap): Int {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
//当在4.4以上手机复用的时候 需要通过此函数获得占用内存
value.allocationByteCount
} else value.byteCount
}
/**
* 设置下载器,这是一个可选的配置项。
*
* @param parser
*/
private fun resetDownloader() {
Log.e("开始设置网络请求", "====")
//缓存文件夹
val cacheFile = File(FileUtils.getCachePath(mContext), "svgaCache")
//网络磁盘缓存大小为50M
val cacheSize = 100L * ByteConstants.MB
//创建缓存对象
val cache = Cache(cacheFile, cacheSize)
val client = OkHttpClient.Builder().cache(cache).build()
parser.fileDownloader = object : SVGAParser.FileDownloader() {
override fun resume(
url: URL,
complete: (inputStream: InputStream) -> Unit,
failure: (e: Exception) -> Unit
): () -> Unit {
val cacheBuild = CacheControl.Builder()
.maxAge(60, TimeUnit.DAYS).build()
val request = Request.Builder().cacheControl(
cacheBuild
).url(url).get().build()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
e.printStackTrace()
failure(e)
}
@Throws(IOException::class)
override fun onResponse(call: Call, response: Response) {
// logger.info("cacheResponse:" + response.cacheResponse() + "networkResponse:" + response.networkResponse())
if (response.body != null)
complete(response.body!!.byteStream())
}
})
return super.resume(url, complete, failure)
}
}
// parser.fileDownloader = object : SVGAParser.FileDownloader() {
// override fun resume(url: URL, complete: Function1, failure: Function1) {
//
//
//
// }
// }
}
}