帧动画frame-by-frame animation
帧动画frame-by-frame animation
逐帧动画 frame-by-frame animation
逐帧动画介绍
Frame-by-frame Animation 主要作用于 view,可以利用 xml 或者代码生成动画,如果使用 xml 方式生成动画需要在 res/drawable
目录下创建动画 xml 文件(animation-list
)。
逐帧动画的原理是一张一张的播放图片资源(drawable 资源),然后出现动画效果。
逐帧动画对应的类是 AnimationDrawable,在 android.graphics.drawable.Drawable 包名下。
逐帧动画使用方式:把逐帧动画作为 view 的背景,然后获取动画,开启动画。
构造函数:
1
AnimationDrawable()
属性说明:
1
2
3
4
oneshot:是否只播放一次,取值true,false,默认为false,用于animation-list
duration:每个item(每一帧动画)播放时长
drawable: 每一帧的drawable资源
visible:drawable资源是否可见,默认不可见
AnimationDrawable 的主要函数:
1
2
3
4
5
6
7
8
addFrame(Drawable frame, int duration)添加drawable
1. frame:每一帧的图片资源
2. duration:每一帧的持续动画
start():开始动画
stop(): 结束动画
isRunning():是否正在执行
setOneShot(boolean oneShot):设置是否只播放一次
getNumberOfFrames() :获取帧的动画数
注意:
内部使用图片作为资源,所以如果图片资源过大可能造成 OOM,虽然简单,但是慎用。
xml 使用
首先定义 animation-list 类型的 drawable frameanimation.xml
1
2
3
4
5
6
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android" android:oneshot="true" >
// drawable= 图片资源;duration = 一帧持续时间(ms)
<item android:drawable="@drawable/d1" android:duration="1000"/>
<item android:drawable="@drawable/d2" android:duration="1000"/>
</animation-list>
设置动画资源的三种使用方式:
第一种:
1
2
3
4
5
// 设置动画
imageView.setImageResource(R.drawable.frameanimation);
// 获取动画对象
animationDrawable = (AnimationDrawable)imageView.getDrawable();
animationDrawable.start();
第二种:
1
2
3
4
// 设置背景:
imageView.setBackgroundResource(R.drawable.frameanimation);
animationDrawable = (AnimationDrawable) imageView.getBackground();
animationDrawable.start();
第三种:
1
2
3
4
// 直接获取然后设置:
animationDrawable = (AnimationDrawable) getResources().getDrawable(R.drawable.frameanimation);
imageView.setBackground(animationDrawable);
animationDrawable.start();
代码使用
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
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
public void startAnimationDrawable() {
//创建帧动画
AnimationDrawable animationDrawable = new AnimationDrawable();
//添加帧
animationDrawable.addFrame(getResources().getDrawable(R.mipmap.record_04), 300);
animationDrawable.addFrame(getResources().getDrawable(R.mipmap.record_05), 300);
animationDrawable.addFrame(getResources().getDrawable(R.mipmap.record_07), 300);
animationDrawable.addFrame(getResources().getDrawable(R.mipmap.record_09), 300);
animationDrawable.addFrame(getResources().getDrawable(R.mipmap.record_11), 300);
//设置动画是否只播放一次, 默认是false
animationDrawable.setOneShot(false);
//根据索引获取到那一帧的时长
int duration = animationDrawable.getDuration(2);
//根据索引获取到那一帧的图片
Drawable drawable = animationDrawable.getFrame(0);
//判断是否是在播放动画
boolean isRunning = animationDrawable.isRunning();
//获取这个动画是否只播放一次
boolean isOneShot = animationDrawable.isOneShot();
//获取到这个动画一共播放多少帧
int framesCount = animationDrawable.getNumberOfFrames();
//把这个动画设置为background,兼容更多版本写下面那句
mIvImg.setBackground(animationDrawable);
mIvImg.setBackgroundDrawable(animationDrawable);
//开始播放动画
animationDrawable.start();
//停止播放动画
animationDrawable.stop();
}
// 注意:我这里的动画时直接写在点击事件里面,如果你想让一看到界面就开始动画,不能叫start( )的方法写在onCreate里面
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
animationDrawable.start();
}
帧动画原理
帧动画使用
1
2
AnimationDrawable animationDrawable = (AnimationDrawable) image.getDrawable();
animationDrawable.start();
AnimationDrawable 执行过程
从 AnimationDrawable.start 开始
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
// AnimationDrawable Android29
public void start() {
mAnimating = true;
if (!isRunning()) {
// Start from 0th frame.
setFrame(0, false, mAnimationState.getChildCount() > 1 || !mAnimationState.mOneShot);
}
}
// 设置当前展示第几帧
private void setFrame(int frame, boolean unschedule, boolean animate) {
if (frame >= mAnimationState.getChildCount()) {
return;
}
mAnimating = animate;
mCurFrame = frame;
selectDrawable(frame); // 展示当前frame帧图,设置当前展示frame drawable
// 如果取消下一帧任务,或者这已经是当前最后一帧,则取消当帧动画任务
if (unschedule || animate) {
unscheduleSelf(this);
}
if (animate) {
// Unscheduling may have clobbered these values; restore them
mCurFrame = frame;
mRunning = true;
// 通过Choreographer监听下一帧的到来
scheduleSelf(this, SystemClock.uptimeMillis() + mAnimationState.mDurations[frame]);
}
}
现在看 selectDrawable
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
public boolean selectDrawable(int index) {
if (index == mCurIndex) {
return false;
}
if (mEnterAnimationEnd != 0 || mExitAnimationEnd != 0) {
if (mAnimationRunnable == null) {
mAnimationRunnable = new Runnable() {
@Override public void run() {
animate(true);
invalidateSelf();
}
};
} else {
unscheduleSelf(mAnimationRunnable);
}
// Compute first frame and schedule next animation.
animate(true);
}
invalidateSelf(); // 绘制自己
return true;
}
void animate(boolean schedule) {
// ...
if (schedule && animating) {
scheduleSelf(mAnimationRunnable, now + 1000 / 60);
}
}
public void scheduleSelf(@NonNull Runnable what, long when) {
final Callback callback = getCallback();
if (callback != null) {
callback.scheduleDrawable(this, what, when);
}
}
// 绘制drawable
public void invalidateSelf() {
final Callback callback = getCallback();
if (callback != null) {
callback.invalidateDrawable(this);
}
}
public void invalidateDrawable(@NonNull Drawable drawable) {
if (verifyDrawable(drawable)) {
final Rect dirty = drawable.getDirtyBounds();
final int scrollX = mScrollX;
final int scrollY = mScrollY;
invalidate(dirty.left + scrollX, dirty.top + scrollY,
dirty.right + scrollX, dirty.bottom + scrollY);
rebuildOutline();
}
}
现在回到 setFrame,调用了 scheduleSelf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// AnimationDrawable
public void scheduleSelf(@NonNull Runnable what, long when) {
final Callback callback = getCallback();
if (callback != null) {
callback.scheduleDrawable(this, what, when);
}
}
// View
public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
if (verifyDrawable(who) && what != null) {
final long delay = when - SystemClock.uptimeMillis();
if (mAttachInfo != null) {
mAttachInfo.mViewRootImpl.mChoreographer.postCallbackDelayed(
Choreographer.CALLBACK_ANIMATION, what, who,
Choreographer.subtractFrameDelay(delay));
} else {
// Postpone the runnable until we know
// on which thread it needs to run.
getRunQueue().postDelayed(what, delay);
}
}
}
通过 Choreographer,监听 vsync 刷新屏幕信号,最后调用到 AnimationDrawable 的 run 方法,设置下一帧的动画
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void run() {
nextFrame(false);
}
private void nextFrame(boolean unschedule) {
int nextFrame = mCurFrame + 1;
final int numFrames = mAnimationState.getChildCount();
final boolean isLastFrame = mAnimationState.mOneShot && nextFrame >= (numFrames - 1);
// Loop if necessary. One-shot animations should never hit this case.
if (!mAnimationState.mOneShot && nextFrame >= numFrames) {
nextFrame = 0;
}
// 新一轮的循环又开始
setFrame(nextFrame, unschedule, !isLastFrame);
}
Drawable.Callback
Drawable.Callback 实现该接口实现动画 drawable
1
2
3
4
5
6
7
8
9
// Drawable Android29
public interface Callback {
// Drawable需要重绘的时候调用,这个时候View需要调用invalidate
void invalidateDrawable(@NonNull Drawable who);
// Drawable调用该方法安排下一帧的动画
void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when);
// 不需要安排下一帧动画了
void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what);
}
CallBack 的绑定
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
// View Android29
public void setBackgroundDrawable(Drawable background) {
if (mBackground != null) {
if (isAttachedToWindow()) {
mBackground.setVisible(false, false);
}
mBackground.setCallback(null);
unscheduleDrawable(mBackground);
}
if (isAttachedToWindow()) {
background.setVisible(getWindowVisibility() == VISIBLE && isShown(), false);
}
applyBackgroundTint();
// Set callback last, since the view may still be initializing.
background.setCallback(this);
invalidate(true);
invalidateOutline();
}
public void setForeground(Drawable foreground) {
if (foreground != null) {
foreground.setLayoutDirection(getLayoutDirection());
if (foreground.isStateful()) {
foreground.setState(getDrawableState());
}
applyForegroundTint();
if (isAttachedToWindow()) {
foreground.setVisible(getWindowVisibility() == VISIBLE && isShown(), false);
}
// Set callback last, since the view may still be initializing.
foreground.setCallback(this);
}
requestLayout();
invalidate();
}
总结
- AnimationDrawable.start,设置第一帧,安排下一帧的监听
- Drawable#scheduleSelf,通过 Drawable.Callback#scheduleDrawable,安排下一帧动画,最终调用到 View 中, 通过监听 vsync 信号的到来,来执行下一帧动画
- Drawable#invalidateSelf,通过 Drawable.Callback#invalidateDrawable,安排 drawable 的绘制
- Drawable.Callback 的实现是 View,在 setBackground 和 setForeground 会调用 Drawable#setCallback 设置回调
案例
帧动画实现新手引导
- 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
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
<?xml version="1.0" encoding="utf-8"?>
<animation-list
xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="false"
android:variablePadding="true">
<item
android:drawable="@drawable/guide_voice_record_00020"
android:duration="100"
android:gravity="center"/>
<item
android:drawable="@drawable/guide_voice_record_00021"
android:duration="100"
android:gravity="center"/>
<item
android:drawable="@drawable/guide_voice_record_00022"
android:duration="100"
android:gravity="center"/>
<item
android:drawable="@drawable/guide_voice_record_00023"
android:duration="100"
android:gravity="center"/>
<item
android:drawable="@drawable/guide_voice_record_00024"
android:duration="100"
android:gravity="center"/>
<item
android:drawable="@drawable/guide_voice_record_00025"
android:duration="100"
android:gravity="center"/>
<item
android:drawable="@drawable/guide_voice_record_00026"
android:duration="100"
android:gravity="center"/>
<item
android:drawable="@drawable/guide_voice_record_00027"
android:duration="100"
android:gravity="center"/>
<item
android:drawable="@drawable/guide_voice_record_00028"
android:duration="100"
android:gravity="center"/>
<item
android:drawable="@drawable/guide_voice_record_00029"
android:duration="100"
android:gravity="center"/>
<item
android:drawable="@drawable/guide_voice_record_00030"
android:duration="100"
android:gravity="center"/>
<item
android:drawable="@drawable/guide_voice_record_00031"
android:duration="100"
android:gravity="center"/>
<item
android:drawable="@drawable/guide_voice_record_00032"
android:duration="100"
android:gravity="center"/>
<item
android:drawable="@drawable/guide_voice_record_00033"
android:duration="100"
android:gravity="center"/>
<item
android:drawable="@drawable/guide_voice_record_00034"
android:duration="100"
android:gravity="center"/>
<item
android:drawable="@drawable/guide_voice_record_00035"
android:duration="100"
android:gravity="center"/>
<item
android:drawable="@drawable/guide_voice_record_00036"
android:duration="100"
android:gravity="center"/>
<item
android:drawable="@drawable/guide_voice_record_00037"
android:duration="100"
android:gravity="center"/>
<item
android:drawable="@drawable/guide_voice_record_00038"
android:duration="100"
android:gravity="center"/>
<item
android:drawable="@drawable/guide_voice_record_00039"
android:duration="100"
android:gravity="center"/>
</animation-list>
- 代码引用
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
public final class VoiceRecordGuideView extends ConstraintLayout implements View.OnTouchListener {
private ImageView mAnimateView;
public VoiceRecordGuideView(Context context) {
this(context, null);
}
public VoiceRecordGuideView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public VoiceRecordGuideView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
LayoutInflater.from(context).inflate(R.layout.layout_guide_voice_record, this, true);
mAnimateView = findViewById(R.id.image);
Drawable anim = null;
try {
@SuppressLint("ResourceType")
Drawable fromXml = Drawable.createFromXml(getResources(), getResources().getXml(R.drawable.anim_guide_voice_record));
if (fromXml instanceof AnimationDrawable) {
anim = new CustomDurationDrawable((AnimationDrawable) fromXml, 50);
} else {
anim = fromXml;
}
} catch (XmlPullParserException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (Exception e) {
}
mAnimateView.setImageDrawable(anim);
setOnTouchListener(this);
setBackgroundColor(CompatUtil.getColor(R.color.mask_color));
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (mAnimateView != null) {
Drawable drawable = mAnimateView.getDrawable();
if (drawable instanceof Animatable) {
((Animatable) drawable).start();
}
}
}
public void show(Activity activity) {
FrameLayout fl = activity.findViewById(android.R.id.content);
if (fl != null) {
fl.addView(this);
}
// ((ViewGroup) activity.getWindow().getDecorView()).addView(this);
}
public void dismiss() {
removeFromWindow();
}
private void removeFromWindow() {
ViewGroup parent = (ViewGroup) getParent();
if (parent != null) {
parent.removeView(this);
}
}
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
removeFromWindow();
break;
default:
break;
}
return true;
}
}
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
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout"
android:gravity="center"
>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="@dimen/sp_16"
android:gravity="center"
android:text="有心事,说出来,我们帮你守护"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:textColor="@color/white"/>
<ImageView
android:id="@+id/image"
android:layout_width="200dp"
android:layout_height="200dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_marginTop="@dimen/qb_px_45"
android:src="@drawable/anim_guide_voice_record"/>
</merge>
本文由作者按照 CC BY 4.0 进行授权