[Android] Optimise Frame Animation

Posted by xiuyuantech on 2020-10-24

帧动画非常容易理解,其实就是简单的由N张静态图片收集起来,
然后我们通过控制依次显示 这些图片,因为人眼"视觉残留"的原因,
会让我们造成动画的"错觉",跟放电影的原理一样!而Android中实现帧动画,
一般我们会用到前面讲解到的一个Drawable:AnimationDrawable 先编写好Drawable,
然后代码中调用start()以及stop()开始或停止播放动画~
当然我们也可以在Java代码中创建逐帧动画,创建AnimationDrawable对象,
然后调用 addFrame(Drawable frame,int duration)向动画中添加帧,接着调用start()和stop()而已~
下面我们来写两个例子体会下帧动画的效果以及熟悉下用法

实现方式

帧动画实现的方式有两种:

  1. 代码方式
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
package com.example;
public class MainActivity extends AppCompatActivity {

private ImageView iv_ani;
private AnimationDrawable mAnimationDrawable;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
iv_ani = (ImageView) findViewById(R.id.iv_ani);
initAnimationDrawable();
}

private void initAnimationDrawable(){
mAnimationDrawable = new AnimationDrawable();
for (int i = 1; i <= 4; i++) {
int id = getResources().getIdentifier("sample_" + i, "mipmap", getPackageName());
Drawable drawable = getResources().getDrawable(id);
mAnimationDrawable.addFrame(drawable, 100);
}
mAnimationDrawable.setOneShot(false);
iv_ani.setImageDrawable(mAnimationDrawable);

}
}

通过AnimationDrawable的addFrame(Drawable frame, int duration) : 添加一帧,并设置该帧显示的持续时间

  1. xml方式

在Drawable资源文件中创建文件frame_animation.xml:

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
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="true">
<item
android:drawable="@mipmap/img_zhuan1"
android:duration="80" />
<item
android:drawable="@mipmap/img_zhuan2"
android:duration="80" />
<item
android:drawable="@mipmap/img_zhuan3"
android:duration="80" />
<!--限于篇幅,省略其他item,自己补上-->
...
</animation-list>

```java

在Activity中设置Drawable并启动动画:
```java
package com.example;
public class MainActivity extends AppCompatActivity {

private ImageView iv_ani;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
iv_ani = (ImageView) findViewById(R.id.iv_ani);
iv_ani.setImageDrawable(R.drawable.frame_animation);
AnimationDrawable animationDrawable = (AnimationDrawable) iv_ani.getDrawable();
animationDrawable.start();
}
}

优缺点

优点:目前这两种方式适合场景简单,图片资源较少的情况。
当动画要求酷炫复杂时,使用这两种方式会导致内存异常

缺点:同时加载多张图片,占用内存资源比较大。

优化方案

网上资源都是通过自定义View实现帧动画,例如:FrameAniamtionView
根据当前需求并兼容已有方案,我是通过自定义View实现了BottomTabBar选中执行一次动画效果。
同时支持本地资源文件和url加载

代码如下:

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
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
package com.example

import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.support.annotation.DrawableRes
import android.support.v7.widget.AppCompatImageView
import android.util.AttributeSet
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.load.resource.gif.GifDrawable
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.Target

class GifOneShotImageView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : AppCompatImageView(context, attrs, defStyleAttr) {
private var hasLoad = false
private var resource: Drawable? = null

/**
* 当前进度
*/
private var progress = 0


/**
* 动画图片
*/
private var bitmapRes = intArrayOf( // 静止

)

/**
* 动画状态
*/
private enum class AnimState {
NONE, // 无状态
JUMP_ANIM // 刷新-cycle跳动
}

/**
* 当前动画状态
*/
private var animState = AnimState.NONE

/**
* 是否正在执行动画
*/
private var isRunning = false

/**
* 动画显示的当前侦
*/
private var currentFrameIndex = 0

/**
* 每侦间隔时间
*/
private var duration: Long = 10

private var loop = false

private var isRes = true

private var iconCount = 0

constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context) : this(context, null)

private val mFilpRunnable = Runnable { startAnimation() }


/**
* 计算下一帧
*
* @param state
*/
private fun computeCurrentFrame(state: AnimState) {
when (state) {
AnimState.NONE -> currentFrameIndex = 0
AnimState.JUMP_ANIM -> {
currentFrameIndex++
if (currentFrameIndex < 0) {
currentFrameIndex = 1
}
if (currentFrameIndex > bitmapRes.size - 1) {
if (loop) {
currentFrameIndex = 1
}
}
}
}
}

fun startResetAnimation() {
stopResetAnimation()
isRunning = true
currentFrameIndex = 0
animState = AnimState.JUMP_ANIM
startAnimation()
}

private fun startAnimation() {
if (isRunning) {
var bitmap: Bitmap? = null
try {
bitmap = BitmapFactory.decodeResource(resources, bitmapRes[currentFrameIndex])
} catch (ex: Exception) {
FLog.e(ex)
}
if (null == bitmap && null != resource) {
if (resource is BitmapDrawable) {
bitmap = (resource as BitmapDrawable).bitmap
} else {
bitmap = BitmapDrawable(resources).bitmap
}
}
if (currentFrameIndex >= bitmapRes.size - 1 && !loop) {
computeCurrentFrame(animState)
setImageBitmap(bitmap)
justStopAnimation()
return
}
computeCurrentFrame(animState)
setImageBitmap(bitmap)
postDelayed(mFilpRunnable, duration)
}
}

private fun justStopAnimation() {
isRunning = false
animState = AnimState.NONE
removeCallbacks(mFilpRunnable)
}

fun stopResetAnimation() {
isRunning = false
currentFrameIndex = 0
animState = AnimState.NONE
removeCallbacks(mFilpRunnable)
}

override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
stopResetAnimation()
}

/**
* 设置进度
* 只有非刷新的状态才根据进度绘制遮挡动画
*
* @param scale
*/
fun setProgress(scale: Float) {
progress = Math.min(Math.max(4, (scale * 360).toInt()), 356)
if (!isRunning) {
invalidate()
}
}

fun setBitmapRes(bitmapRes: IntArray?) {
this.bitmapRes = bitmapRes ?: intArrayOf()
}

fun isRunning(): Boolean {
return isRunning
}

fun setIsRes(isRes: Boolean) {
this.isRes = isRes
}

fun setDefaultDrawable(drawable: Drawable?) {
this.resource = drawable
}

fun setDefaultBitmap(drawable: Bitmap?) {
setDefaultDrawable(BitmapDrawable(drawable))
}

fun setNavigation(navigation: Navigation?) {
this.mNavigation = navigation
}

fun getCurrentFrameIndex(): Int {
return currentFrameIndex
}

fun hasInited(): Boolean {
return 0 != currentFrameIndex || hasLoad
}

fun setLoop(loop: Boolean) {
this.loop = loop
}

fun loadGifOneshot(url: String?, @DrawableRes errorDrawble: Int) {
if (hasLoad) return
GlideApp.with(context)
.load(url)
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.error(errorDrawble)
.listener(GifImageViewRequest(1))
.into(this)
}

fun loadGifOneshot(url: String?, errorDrawble: Bitmap) {
if (hasLoad) return
GlideApp.with(context)
.load(url)
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.error(BitmapDrawable(errorDrawble))
.listener(GifImageViewRequest(1))
.into(this)
}

override fun setImageBitmap(bm: Bitmap?) {
super.setImageBitmap(bm)
if (hasLoad) {
hasLoad = false
}
}

override fun setImageDrawable(drawable: Drawable?) {
super.setImageDrawable(drawable)
if (hasLoad) {
hasLoad = false
}
}

override fun setImageResource(resId: Int) {
super.setImageResource(resId)
currentFrameIndex = 0
if (hasLoad) {
hasLoad = false
}
}

fun resetImageBitmap(bitmap: Bitmap?) {
currentFrameIndex = 0
justStopAnimation()
setImageBitmap(bitmap)
}

inner class GifImageViewRequest(var loopCount: Int) : RequestListener<Drawable> {
override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Drawable>?, isFirstResource: Boolean): Boolean {
hasLoad = false
return false
}

override fun onResourceReady(resource: Drawable, model: Any?, target: Target<Drawable>?, dataSource: DataSource?, isFirstResource: Boolean): Boolean {
this@GifOneShotImageView.resource = resource
if (resource is GifDrawable) {
val gifDrawable = resource as GifDrawable
gifDrawable.setLoopCount(loopCount)
}
hasLoad = true
return false
}

}
}

效果如下:

帧动画

总结

生产环境下不适合使用下原生提供的实现方式,容易产生问题。
推荐大家使用优化版自定义方式来实现帧动画,节约内存,
也不易发生OOM。注意一点bitmap用完不再使用时及时回收。