Android自定义控件之GifView

最近的项目需要在主界面显示Gif动图,于是查了一下资料,一般是使用开源框架 Glideandroid-gif-drawable ,前者加载速度较慢,并且没有单独的Gif播放与暂停接口,后者使用JNI加载,不会出现OOM问题,速度更快,性能更优。

由于我对自定义控件这方面了解不深,所以想趁这个机会刚好学习一下,自己写一个可以流畅显示Gif动图并能控制播放的GifView控件。

自定义控件一般有以下三种方式:

  • 组合原生控件

    使用几个基本控件组合在一起,形成一个新的控件。这种方式通常都需要继承一个合适的 ViewGroup,再给它添加指定功能的控件,形成新的空间。通过这种方式创建的控件我们还可以给它指定一些可配置的属性,增强它的可操控性。比如很多应用中普遍使用的标题栏控件。

  • 继承原生控件

    继承已有的控件,创建新控件,保留继承的父控件的特性,并且还可以引入新特性。

  • 重写:自绘控件

    如果继承原生控件或者是组合原生控件都不能满足我们的特殊需求,这种时候就只能够自己重头写一个全新的控件了。创建一个全新的 View 重点在于绘制和交互的部分,通常需要继承 View 类,并重写 onDraw() 、onMeasure() 等方法,还可以像刚才的组合控件一样,引入自定义属性来丰富控件的可控性。

实践内容参考此链接,这里不再赘述。

而这一次的GifView自定义控件则采取第三种方式:自绘

首先我们先了解Android自带的类:android.graphics.Movie。它管理着Gif动画中的多个帧,可以将其加载并播放,我们只要换算好时间关系,通过setTime()让它在draw()的时候绘制出对应的帧图像,即可实现Gif播放的效果。

在动手之前,先通过官网文档了解 android.graphics.Movie 这个类,要养成一种阅读官方资料或源码的习惯,在足够了解的基础上才能够更好地进行二次创造。

本来是想自己动手写的,但是发现Github上已经有人很好地实现了…所以我打算直接跟着他的代码进行讲解…(没错其实是我想偷懒orz)

这是源码地址:https://github.com/Cutta/GifView

首先,在res/values目录下添加自定义属性,进行属性配置:

attrs.xml

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="GifView">
<attr name="gif" format="reference" />
<attr name="paused" format="boolean" />
</declare-styleable>

<declare-styleable name="CustomTheme">
<attr name="gifViewStyle" format="reference" />
</declare-styleable>
</resources>

如果你对自定义控件的属性配置不够了解,可以阅读博客1或者博客2

然后,在继承View的基础上开始编写我们的GifView了。

GifView.class

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
package com.example.yc.androidsrc;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Movie;
import android.os.Build;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;

/**
* 自定义控件,用于显示Gif动图
* Created by yc on 2018/11/18.
*/

public class GifView extends View {

private static final int DEFAULT_MOVIE_VIEW_DURATION = 1000; // 默认1秒

private int mMovieResourceId;
private Movie movie;

private long mMovieStart;
private int mCurrentAnimationTime;

private float mLeft;
private float mTop;

private float mScale;

private int mMeasuredMovieWidth;
private int mMeasuredMovieHeight;

private volatile boolean mPaused;
private boolean mVisible = true;

/**
* 构造函数
*/
public GifView(Context context) {
this(context, null);
}

public GifView(Context context, AttributeSet attrs) {
this(context, attrs, R.styleable.CustomTheme_gifViewStyle);
}

public GifView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);

setViewAttributes(context, attrs, defStyle);
}

@SuppressLint("NewApi")
private void setViewAttributes(Context context, AttributeSet attrs, int defStyle) {

// 从 HONEYCOMB(Api Level:11) 开始,必须关闭HW加速度才能在Canvas上绘制Movie
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
}
// 从描述文件中读出Gif的值,绘制出Movie实例
final TypedArray array = context.obtainStyledAttributes(attrs,
R.styleable.GifView, defStyle, R.style.Widget_GifView);

mMovieResourceId = array.getResourceId(R.styleable.GifView_gif, -1); // -1为默认值
mPaused = array.getBoolean(R.styleable.GifView_paused, false);

array.recycle();

if (mMovieResourceId != -1) {
movie = Movie.decodeStream(getResources().openRawResource(mMovieResourceId));
}
}

/**
* 设置Gif资源
*/
public void setGifResource(int movieResourceId) {
this.mMovieResourceId = movieResourceId;
movie = Movie.decodeStream(getResources().openRawResource(mMovieResourceId));
requestLayout();
}

/**
* 获取Gif资源
*/
public int getGifResource() {
return this.mMovieResourceId;
}

/**
* 播放
*/
public void play() {
if (this.mPaused) {
this.mPaused = false;

/**
* 计算新的movie开始时间,使它从刚刚停止的帧重新播放
*/
mMovieStart = android.os.SystemClock.uptimeMillis() - mCurrentAnimationTime;

invalidate();
}
}

/**
* 暂停
*/
public void pause() {
if (!this.mPaused) {
this.mPaused = true;

invalidate();
}

}

/**
* 判断Gif动图当前处于播放还是暂停状态
*/

public boolean isPaused() {
return this.mPaused;
}

public boolean isPlaying() {
return !this.mPaused;
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

if (movie != null) {
int movieWidth = movie.width();
int movieHeight = movie.height();

/**
* 计算水平方向上的扩展
*/
float scaleH = 1f;
int measureModeWidth = MeasureSpec.getMode(widthMeasureSpec);

if (measureModeWidth != MeasureSpec.UNSPECIFIED) {
int maximumWidth = MeasureSpec.getSize(widthMeasureSpec);
if (movieWidth > maximumWidth) {
scaleH = (float) movieWidth / (float) maximumWidth;
}
}

/**
* 计算竖直方向上的扩展
*/
float scaleW = 1f;
int measureModeHeight = MeasureSpec.getMode(heightMeasureSpec);

if (measureModeHeight != MeasureSpec.UNSPECIFIED) {
int maximumHeight = MeasureSpec.getSize(heightMeasureSpec);
if (movieHeight > maximumHeight) {
scaleW = (float) movieHeight / (float) maximumHeight;
}
}

/**
* 计算扩展规模
*/
mScale = 1f / Math.max(scaleH, scaleW);

mMeasuredMovieWidth = (int) (movieWidth * mScale);
mMeasuredMovieHeight = (int) (movieHeight * mScale);

setMeasuredDimension(mMeasuredMovieWidth, mMeasuredMovieHeight);

} else {
/**
* Movie为空,设置最小可用大小
*/
setMeasuredDimension(getSuggestedMinimumWidth(), getSuggestedMinimumHeight());
}
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
/**
* 计算距离,以便绘制动画帧
*/
mLeft = (getWidth() - mMeasuredMovieWidth) / 2f;
mTop = (getHeight() - mMeasuredMovieHeight) / 2f;

mVisible = getVisibility() == View.VISIBLE;
}

@Override
protected void onDraw(Canvas canvas) {
if (movie != null) {
if (!mPaused) {
updateAnimationTime();
drawMovieFrame(canvas);
invalidateView();
} else {
drawMovieFrame(canvas);
}
}
}


@SuppressLint("NewApi")
private void invalidateView() {
if (mVisible) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
postInvalidateOnAnimation();
} else {
invalidate();
}
}
}

/**
* 计算当前动画时间
*/
private void updateAnimationTime() {
long now = android.os.SystemClock.uptimeMillis();
// 如果是第一帧,记录起始时间
if (mMovieStart == 0) {
mMovieStart = now;
}
// 取出动画的时长
int dur = movie.duration();
if (dur == 0) {
dur = DEFAULT_MOVIE_VIEW_DURATION;
}
// 算出需要显示第几帧
mCurrentAnimationTime = (int) ((now - mMovieStart) % dur);
}

/**
* 绘制当前要显示的Gif帧
*/
private void drawMovieFrame(Canvas canvas) {

movie.setTime(mCurrentAnimationTime);

canvas.save(Canvas.MATRIX_SAVE_FLAG);
canvas.scale(mScale, mScale);
movie.draw(canvas, mLeft / mScale, mTop / mScale);
canvas.restore();
}

@SuppressLint("NewApi")
@Override
public void onScreenStateChanged(int screenState) {
super.onScreenStateChanged(screenState);
mVisible = screenState == SCREEN_STATE_ON;
invalidateView();
}

@SuppressLint("NewApi")
@Override
protected void onVisibilityChanged(View changedView, int visibility) {
super.onVisibilityChanged(changedView, visibility);
mVisible = visibility == View.VISIBLE;
invalidateView();
}

@Override
protected void onWindowVisibilityChanged(int visibility) {
super.onWindowVisibilityChanged(visibility);
mVisible = visibility == View.VISIBLE;
invalidateView();
}

}

使用方式:

  1. 直接在xml布局文件中设置该控件的gif属性指向哪个资源

    1
    2
    3
    <com.example.yc.androidsrc.GifView
    app:gif="@drawable/rain"
    ... />
  2. 在activity中通过setGifResource(int movieResourceId)进行设置

    1
    2
    final GifView gifV = (GifView) findViewById(R.id.gifV);
    gifV.setGifResource(R.drawable.rain);

效果图(录制屏幕后再转成Gif导致有点失真了orz 勉强看看):