Android 同频共帧动画效果 同频共帧 现状 & 痛点

同频共帧

我们听过“同频共振”,其原理是多个物体物体以同样的频率振动。本篇实现的效果是“同频共帧”,两者有一定的联系,但属于不同的概念。

“同频共帧”的含义是:一种动画效果,以同样的频率、同样的画面展示在多个不同View上。

特点:

  • 动画效果
  • 同样的频率
  • 同一帧 (严格意义上是小于1个vsync信号的帧)
  • 多个不同View同时展示

下面我们使用6个ImageView来同步播放同一个动画。

效果1:

fire_95.gif

效果2:

fire_96.gif

看似这两种动画就是简单的动画,你可能会想,直接创建6个AnimationDrawable不就行了?显然这种做法仅仅适合图片数量有限或者内存较大的设备,另外AnimationDrawable越多,低性能设备由于CPU性能差,可能出现帧不一致的情况。原则上说,本篇是属于动画性能优化的文章。

场景:
我们看一些综艺节目,电视两侧都会出现各种火焰、礼花等效果,而且左右是对称的,想象一下,这种情况如果你给一个超大图片去实现,这个时候占用内存肯定会很大,因此是最好的方式是让动画绘制区域更小。

这种动效其实在手机版QQ上就有,如果你给自己的头像设置为一个动态图,如果在聊天群连发多条消息,那么你就会发现,在同一个页面上你的头像动画是同步同帧率展示的。

现状 & 痛点

现状

我们以帧动画问题展开,要知道帧动画有难以容忍的内存占用问题、以及主线程解码问题,同时包体积问题也相当严重,为此市面上出现了很多方案。libpag、lottie、VapPlayer、AlphaPlayer、APNG、GIF、SVGA、AnimationDrawable等。但你在开发时就会发现,每一种引擎都有自己独特的优势,也有自己独特的劣势,你往往想着用一种引擎统一所有动效实现,但往往现实不允许。

我们来说说几大引擎的优缺点:

libPag: 目前支持功能最多的动效引擎,普通动画性能也非常不错,相比其他引擎快很多。该引擎使用自研渲染引擎和解码器实现,但是对于预合成动效(超长动效和复杂动效可能会用到),由于其使用的是软解,在低配设备上比VapPlayer和AlphaPlayer卡的多,另外lib so相比其他引擎也是大很多。

VapPlayer/AlphaPlayer : 这两种其都是通过alpha 遮罩实现,大部分情况下使用的是设备硬解码器,不过,VapPlayer缺乏硬解码器筛选机制,偶尔有时会拿到软解码器,另外其本身存在音画同步问题,至于AlphaPlayer把播放交给系统和三方播放器,避免了此类问题。但是,如果是音视频类app,他们都有共同的问题,终端设备上硬解码器的实例数量是受限制的,甚至有的设备解码器同一时刻只能使用一个,同时使用这种解码器就会造成业务播放器绿屏、起播失败、解码器卡住等问题。不过解决办法是将特效和业务播放器资源类型隔离,如果业务播放器是使用h264资源,那么动效播放可以使用h265、mpeg2、av1等其他编码类型资源,以此规避解码器竞争。

lottie: lottie目前是比较广为人知的动效引擎,使用也相当广泛,上手难度也比较低,可以解决大部分动效播放问题。但lottie动效也存在一些跨平台兼容性,需要单独适配,其次缺少很多特效支持,性能方面,一般情况下其性能是不如libpag的。不过总体能覆盖到大部分场景,至于兼容性问题每个平台单独提供不同的lottie规则即可。

其实,lottie开发也有一定的难度。开发中常常会遇到的问题是,UI设计人员对于lottie的compose layer理解存在问题,往往会出现将lottie动画做成和帧动画一样的动画,显然,compose layer的思想是多张图片合成,那就意味着图片本身应该有大有小,按一定轨迹运动和渐变,而不是类似帧动画一样一帧一帧简单播放。

APNG、GIF、 WebP: 这类动画属于资源型动画,其本身存在很多缺点,比如占内存和耗cpu,另外APNG和WebP兼容性不足,不过简短的动效的还是可以使用的,特别是WebP在Android、Web、部分iOS设备上优势也比较大。

SVGA:很多平台对这种动画抱有期待,特别是其矢量性质和低内存的特点,然而,其本身面临标准不统一的问题,造成跨平台的能力不足,其次是因为SVGA需要转为Path绘制,但Path的绘制非常消耗性能。

LazyAnimationDrawable:几乎所有的动画对低配设备都不友好,帧动画比上不足比下有余,低配设备上,为了解决libpag、VapPlayer、lottie对低配设备上音视频类app不友好的问题,使用AnimationDrawble显然是不行的,因此我们往往会实现了自己的AnimationDrawable,使其具备兜底的能力,主要原理: 独立线程解码 + 展示一帧预 + 加载下一帧 + 帧缓存,其实也就是LazyAnimationDrawable。

痛点

以上我们罗列了很多问题,看似和我们的主要目的毫无关系,其实我们可以想想,如果使用上述引擎,哪种方式可以实现兼容性更好的“同频共帧”动效呢 ?

实际上,几乎没有引擎能承担此任务,那有没有办法实现呢?

原理

我们很难让每个View同时执行和绘制同样的画面,另一个问题是,如果设计多个View绘制Bitmap,那么还可能造成资源加载的内存OOM的问题。另外一方面如果使用LazyAnimationDrawable、VapX、AlphaPlayer等,同时执行相同的动效,那么解码线程需要创建多个,显然性能、内存问题也是重中之重。

有没有更加简单方法呢 ?

实际上是有的,那就是投影。

我们无论使用CompositeDrawable、LazyAnimationDrawable、AnimationDrawable还是VectorDrawable,我们可以保证在使用个实例的情况下,将画面绘制到不同View上即可。

不过:本篇以AnimationDrawable 为例子实现,其实其他Drawable动画类似。

实现

这种难度也是很高的,如果我们使用一个View 管理器,然后构建一个播放器,显然还要处理View各种状态,显然避免不了耦合问题。这里我们回到开头说过的drawable方案,当然,一个drawable显然无法设置给多个View,这点显然是我们需要处理的难点,此外,每个View的大小也不一致,如何处理这种问题呢。

之前的文章中我们实现了很多动效,但几乎都是基于View本身实现的,但是在Android中,Drawable最容易扩展和移植的渲染组件。通过Drawable提供的接口,我们可以接入libpag、lottie、SVG、APNG、gif,LazyAnimationDrawable、AnimationDrawable等动效,更加方便移植,同时Drawable支持setHotspot和setState接口,可以实现复杂度较低的交互效果。

Drawable Wrapper

一个Drawable不能同时设置给不同的View,但是我们可以创建多个Drawable从目标Drawable上“投影”画面到自身。

这里我们参考Glide中com.bumptech.glide.request.target.FixedSizeDrawable 实现,其原理是通过FixedSizeDrawable代理真实的drawble绘制,从而达到投影效果。在drawable更新时,利用Matrix实现Canvas缩放,即可适配不同大小的View。

1
2
3
4
5
6
7
8
9
10
11
java复制代码FixedSizeDrawable(State state, Drawable wrapped) {
this.state = Preconditions.checkNotNull(state);
this.wrapped = Preconditions.checkNotNull(wrapped);

// We will do our own scaling.
wrapped.setBounds(0, 0, wrapped.getIntrinsicWidth(), wrapped.getIntrinsicHeight());

matrix = new Matrix();
wrappedRect = new RectF(0, 0, wrapped.getIntrinsicWidth(), wrapped.getIntrinsicHeight());
bounds = new RectF();
}

Matrix 的作用:

这里主要是将原有的画面缩放至目标View

1
2
java复制代码matrix.setRectToRect(wrappedRect, drawableBounds, Matrix.ScaleToFit.CENTER);
canvas.concat(matrix); //Canvas Matrix 转换

当然,必要时支持下alpha和colorFilter,以此来实现变色效果,下面是完整实现。

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
java复制代码public static class AnimationDrawableWrapper extends Drawable {

private final Drawable animationDrawable; //动画drawable
private final Matrix matrix = new Matrix();
private final RectF wrappedRect;
private final RectF drawableBounds;
private final Matrix.ScaleToFit scaleToFit;
private int alpha = 255;
private ColorFilter colorFilter;

public AnimationDrawableWrapper(Drawable drawable, Matrix.ScaleToFit scaleToFit) {
this.animationDrawable = drawable;
this.wrappedRect = new RectF(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
this.drawableBounds = new RectF();
this.scaleToFit = scaleToFit;
}

@Override
public void draw(Canvas canvas) {
Drawable current = animationDrawable.getCurrent();
if (current == null) {
return;
}
current.setAlpha(this.alpha);
current.setColorFilter(this.colorFilter);
Rect drawableRect = current.getBounds();
wrappedRect.set(drawableRect);
drawableBounds.set(getBounds());

// 变化坐标
matrix.setRectToRect(wrappedRect, drawableBounds, scaleToFit);

int save = canvas.save();

canvas.concat(matrix);
current.draw(canvas);

canvas.restoreToCount(save);

current.setAlpha(255);//还原
current.setColorFilter(null); //还原

}

@Override
public void setAlpha(int alpha) {
this.alpha = alpha;
}

@Override
public void setColorFilter(ColorFilter colorFilter) {
this.colorFilter = colorFilter;
}

@Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}

}

View更新

我们知道AnimationDrawable每一帧都是不一样的,那怎么将每一帧都能绘制在View上呢,了解过Drawable更新机制的开发者都知道,每一个View都实现了Drawable.Callback,当给View设置drawable时,Drawable.Callback也会设置给drawable。

Drawable刷新View时需要调用invalidate,显然是通过Drawable.Callback实现,当然,Drawable自身就实现了更新方法Drawable#invalidateSelf,我们只需要调用改方法刷新View即可触发View#onDraw,从而触发drawable#draw方法。

1
2
3
4
5
6
java复制代码public void invalidateSelf() {
final Callback callback = getCallback();
if (callback != null) {
callback.invalidateDrawable(this);
}
}

更新AnimationDrawable

显然,任何动画都具备时间属性,因此更新Drawable是必要的,View本身是可以通过Drawable.Callback机制更新Drawable的。通过scheduleDrawable和unscheduleDrawable 定时处理Runnable和取消Runnable。

1
2
3
4
5
6
7
8
java复制代码public interface Callback {

void invalidateDrawable(@NonNull Drawable who);

void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when);

void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what);
}

而AnimationDrawable实现了Runnable接口

1
2
3
4
java复制代码@Override
public void run() {
nextFrame(false);
}

然而,如果使用的RecyclerView,那么还可能会出现View 从页面移除的问题,因此依靠View显然是不行的,这里我们引入Handler或者Choreograper。

1
java复制代码this.choreographer = Choreographer.getInstance();

但是,我们什么时候调用呢?显然还得利用Drawable.Callback机制

给animationDrawable设置Drawable.Callback

1
java复制代码this.drawable.setCallback(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
java复制代码@Override
public void invalidateDrawable(@NonNull Drawable who) {
//更新所有wrapper
for (int i = 0; i < drawableList.size(); i++) {
WeakReference<AnimationDrawableWrapper> reference = drawableList.get(i);
AnimationDrawableWrapper wrapper = reference.get();
if (wrapper == null) {
return;
}
wrapper.invalidateSelf();
}
}

@Override
public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
this.scheduleTask = what;
this.choreographer.postFrameCallbackDelayed(this, when - SystemClock.uptimeMillis());
}

@Override
public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
this.scheduleTask = null;
this.choreographer.removeFrameCallback(this);
}

既然使用Choreographer,那doFrame需要实现的

1
2
3
4
5
6
java复制代码@Override
public void doFrame(long frameTimeNanos) {
if(this.scheduleTask != null) {
this.scheduleTask.run();
}
}

好了,以上就是核心逻辑,到此我们就实现了核心逻辑

完整代码

下面是镜像动画的完整代码,因为是比较成熟的代码,接入了LazyAnimationDrawable。

当然,如果你没有LazyAnimationDrawable的实现的话,删掉LazyAnimationDrawable的引,仅仅通过AnimationDrawable也是可以正常使用的。另外,你还可添加其他动画实现,比如VerctorDrawable等。

我们通过MirrorFrameAnimation,实现同频共帧动画效果。

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
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
java复制代码public class MirrorFrameAnimation implements Drawable.Callback, Choreographer.FrameCallback {
private final Drawable drawable;
private final int drawableWidth;
private final int drawableHeight;
private List<WeakReference<AnimationDrawableWrapper>> drawableList = new ArrayList<>();
private Choreographer choreographer;
private Runnable scheduleTask;

public MirrorFrameAnimation(Resources resources, int resId, boolean isLazyDrawableAnimation, int drawableWidth, int drawableHeight) {

//设置宽高,防止AnimationDrawable大小不稳定问题
this.drawableWidth = drawableWidth;
this.drawableHeight = drawableHeight;
this.drawable = isLazyDrawableAnimation ? LazyAnimationDrawableInflater.getDrawable(resources, resId, null) : resources.getDrawable(resId);
this.drawable.setBounds(0, 0, drawableHeight, drawableHeight);
this.drawable.setCallback(this);
this.choreographer = Choreographer.getInstance();
}

public MirrorFrameAnimation(LazyAnimationDrawable lazyAnimationDrawable, int drawableWidth, int drawableHeight) {

//设置宽高,防止AnimationDrawable大小不稳定问题
this.drawableWidth = drawableWidth;
this.drawableHeight = drawableHeight;
this.drawable = lazyAnimationDrawable;
this.drawable.setBounds(0, 0, drawableHeight, drawableHeight);
this.drawable.setCallback(this);
this.choreographer = Choreographer.getInstance();
}

public MirrorFrameAnimation(AnimationDrawable animationDrawable, int drawableWidth, int drawableHeight) {

//设置宽高,防止AnimationDrawable大小不稳定问题
this.drawableWidth = drawableWidth;
this.drawableHeight = drawableHeight;
this.drawable = animationDrawable;
this.drawable.setBounds(0, 0, drawableHeight, drawableHeight);
this.drawable.setCallback(this);
this.choreographer = Choreographer.getInstance();
}

public void start() {
choreographer.removeFrameCallback(this);
if (drawable instanceof AnimationDrawable) {
((AnimationDrawable) drawable).start();
} else if (drawable instanceof LazyAnimationDrawable) {
((LazyAnimationDrawable) drawable).start();
}
}

public void stop() {
choreographer.removeFrameCallback(this);
if (drawable instanceof AnimationDrawable) {
((AnimationDrawable) drawable).stop();
} else if (drawable instanceof LazyAnimationDrawable) {
((LazyAnimationDrawable) drawable).stop();
}
}

/**
* @return The number of frames in the animation
*/
public int getNumberOfFrames() {
if (drawable instanceof AnimationDrawable) {
return ((AnimationDrawable) drawable).getNumberOfFrames();
} else if (drawable instanceof LazyAnimationDrawable) {
return ((LazyAnimationDrawable) drawable).getNumberOfFrames();
}
return 0;
}

/**
* @return The Drawable at the specified frame index
*/
public Drawable getFrame(int index) {
if (drawable instanceof AnimationDrawable) {
return ((AnimationDrawable) drawable).getFrame(index);
} else if (drawable instanceof LazyAnimationDrawable) {
return ((LazyAnimationDrawable) drawable).getDrawableOfFrame(index);
}
return drawable;
}

/**
* @return The duration in milliseconds of the frame at the
* specified index
*/
public int getDuration(int index) {
if (drawable instanceof AnimationDrawable) {
return ((AnimationDrawable) drawable).getDuration(index);
} else if (drawable instanceof LazyAnimationDrawable) {
return ((LazyAnimationDrawable) drawable).getDuration(index);
}
return 0;
}

/**
* @return True of the animation will play once, false otherwise
*/
public boolean isOneShot() {
if (drawable instanceof AnimationDrawable) {
return ((AnimationDrawable) drawable).isOneShot();
} else if (drawable instanceof LazyAnimationDrawable) {
return ((LazyAnimationDrawable) drawable).isOneShot();
}
return true;
}

public boolean isRunning() {
if (drawable instanceof AnimationDrawable) {
return ((AnimationDrawable) drawable).isRunning();
} else if (drawable instanceof LazyAnimationDrawable) {
return ((LazyAnimationDrawable) drawable).isRunning();
}
return false;
}

public long getDuration(){
long duration = 0;
if (drawable instanceof AnimationDrawable) {
AnimationDrawable animationDrawable = (AnimationDrawable) drawable;
int numberOfFrames = animationDrawable.getNumberOfFrames();
for (int i = 0; i < numberOfFrames; i++) {
duration += animationDrawable.getDuration(i);
}
} else if (drawable instanceof LazyAnimationDrawable) {
LazyAnimationDrawable animationDrawable = (LazyAnimationDrawable) drawable;
int numberOfFrames = animationDrawable.getNumberOfFrames();
for (int i = 0; i < numberOfFrames; i++) {
duration += animationDrawable.getDuration(i);
}
}
return duration;
}

public boolean isEndFrame() {
if (drawable instanceof AnimationDrawable) {
AnimationDrawable animationDrawable = (AnimationDrawable) drawable;
int frameIndex = animationDrawable.getNumberOfFrames() - 1;
return animationDrawable.isOneShot() && animationDrawable.getFrame(frameIndex) == animationDrawable.getCurrent();
}
if (drawable instanceof LazyAnimationDrawable) {
LazyAnimationDrawable animationDrawable = (LazyAnimationDrawable) drawable;
int frameIndex = animationDrawable.getNumberOfFrames() - 1;
if (animationDrawable.isOneShot() && animationDrawable.getFrame(frameIndex) == animationDrawable.getCurrentFrame()) {
return true;
}
}
return false;
}

public boolean isEndAnimation() {
if (isEndFrame()) {
return true;
}
if (drawable instanceof LazyAnimationDrawable) {
LazyAnimationDrawable animationDrawable = (LazyAnimationDrawable) drawable;
if (animationDrawable.isLoopFinish()) {
return true;
}
}
return false;
}

/**
* Sets whether the animation should play once or repeat.
*
* @param oneShot Pass true if the animation should only play once
*/
public void setOneShot(boolean oneShot) {
if (drawable instanceof AnimationDrawable) {
((AnimationDrawable) drawable).setOneShot(oneShot);
} else if (drawable instanceof LazyAnimationDrawable) {
((LazyAnimationDrawable) drawable).setOneShot(oneShot);
}
}

public void syncDrawable(View view, Matrix.ScaleToFit scaleToFit) {
if (!(drawable instanceof AnimationDrawable) && !(drawable instanceof LazyAnimationDrawable)) {
if (view instanceof ImageView) {
((ImageView) view).setImageDrawable(drawable);
} else {
view.setBackground(drawable);
}
return;
}

AnimationDrawableWrapper wrapper = new AnimationDrawableWrapper(drawable,scaleToFit);
drawableList.add(new WeakReference<>(wrapper));

if (view instanceof ImageView) {
((ImageView) view).setImageDrawable(wrapper);
} else {
view.setBackground(wrapper);
}
}

@Override
public void invalidateDrawable(Drawable who) {
for (int i = 0; i < drawableList.size(); i++) {
WeakReference<AnimationDrawableWrapper> reference = drawableList.get(i);
AnimationDrawableWrapper wrapper = reference.get();
if (wrapper == null) {
return;
}
wrapper.invalidateSelf();
}
}

@Override
public void scheduleDrawable(Drawable who, Runnable what, long when) {
this.scheduleTask = what;
this.choreographer.postFrameCallbackDelayed(this, when - SystemClock.uptimeMillis());
}

@Override
public void unscheduleDrawable(Drawable who, Runnable what) {
this.scheduleTask = null;
this.choreographer.removeFrameCallback(this);
}


@Override
public void doFrame(long frameTimeNanos) {
if (this.scheduleTask != null) {
this.scheduleTask.run();
}
}

@Nullable
public MirrorFrameAnimation cloneDrawable(int drawableWidth,int drawableHeight) {
Drawable newDrawable = drawable.getConstantState().newDrawable();
if(newDrawable instanceof LazyAnimationDrawable) {
return new MirrorFrameAnimation((LazyAnimationDrawable) newDrawable, drawableWidth, drawableHeight);
}else if(newDrawable instanceof AnimationDrawable) {
return new MirrorFrameAnimation((AnimationDrawable) newDrawable, drawableWidth, drawableHeight);
}
return null;
}

public static class AnimationDrawableWrapper extends Drawable {

private final Drawable animationDrawable; //动画drawable
private final Matrix matrix = new Matrix();
private final RectF wrappedRect;
private final RectF drawableBounds;
private final Matrix.ScaleToFit scaleToFit;
private int alpha = 255;
private ColorFilter colorFilter;

public AnimationDrawableWrapper(Drawable drawable, Matrix.ScaleToFit scaleToFit) {
this.animationDrawable = drawable;
this.wrappedRect = new RectF(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
this.drawableBounds = new RectF();
this.scaleToFit = scaleToFit;
}

@Override
public void draw(Canvas canvas) {
Drawable current = animationDrawable.getCurrent();
if (current == null) {
return;
}
current.setAlpha(this.alpha);
current.setColorFilter(this.colorFilter);
Rect drawableRect = current.getBounds();
wrappedRect.set(drawableRect);
drawableBounds.set(getBounds());

// 变化坐标
matrix.setRectToRect(wrappedRect, drawableBounds, scaleToFit);

int save = canvas.save();

canvas.concat(matrix);
current.draw(canvas);

canvas.restoreToCount(save);

current.setAlpha(255);//还原
current.setColorFilter(null); //还原

}

@Override
public void setAlpha(int alpha) {
this.alpha = alpha;
}
@Override
public void setColorFilter(ColorFilter colorFilter) {
this.colorFilter = colorFilter;
}
@Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}

}
}

使用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码int dp2px = (int) dp2px(100);
MirrorFrameAnimation mirrorFrameAnimation = new MirrorFrameAnimation(getResources(),R.drawable.loading_animation,dp2px,dp2px);

mirrorFrameAnimation.syncDrawable(imageView1,Matrix.ScaleToFit.CENTER);
mirrorFrameAnimation.syncDrawable(imageView2,Matrix.ScaleToFit.CENTER);
mirrorFrameAnimation.syncDrawable(imageView3,Matrix.ScaleToFit.FILL);
mirrorFrameAnimation.syncDrawable(imageView4,Matrix.ScaleToFit.FILL);
mirrorFrameAnimation.syncDrawable(imageView5,Matrix.ScaleToFit.CENTER);
mirrorFrameAnimation.syncDrawable(imageView6,Matrix.ScaleToFit.CENTER);

mStart.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mirrorFrameAnimation.start();
}
});
mStop.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mirrorFrameAnimation.stop();
}
});

适用范围

图像同步执行需求

我们常常会出现屏幕边缘方向同时展示相同动画的问题,由于每个动画启动存在一定的延时,以及控制逻辑不稳定,往往会出现一边动画播放结束,另一边动画还在展示的情况。

本篇我们实现了“同频共帧动效”,实际上这也是一种对称动画的优化方法。

总结

动效一直是Android设备的上需要花大力气优化的,如果是图像同步执行、对称动效,本篇方案显然可以帮助我们减少线程和内存的消耗。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%