我现在要自定义一个ImageView,用来显示Gif图片
自定义View,是肯定需要重写构造方法的。
public class MyGifView extends ImageView {
public MyGifView(final Context context, final AttributeSet attrs, final int defStyle) {
super(context, attrs, defStyle);
}
public MyGifView(final Context context, final AttributeSet attrs) {
super(context, attrs);
}
public MyGifView(final Context context) {
super(context);
}
}
虽然是自定义view,但现在等于什么都没写,等于还是原来那个ImageView。然后在main.xml中简单引入,用法和ImageView一样。
<com.azz.mygifview.MyGifView
android:layout_width = "wrap_content"
android:layout_height = "wrap_content"
android:src = "@drawable/coffee"/>
coffee是一张gif动图,此时运行,就显示coffee的第一帧图片。
构造方法参数列表里有个AttributeSet attrs
属性我很在意,不明白它的具体含义,看源码之后查到了两个方法
int getAttributeCount() //得到属性个数
String getAttributeName(int index) //得到相应下标的属性名
通过这两个方法简单结合,我得到了答案
public MyGifView(final Context context, final AttributeSet attrs) {
super(context, attrs);
int count = attrs.getAttributeCount();
for (int i = 0; i < count; i++) {
Log.d(TAG, "attrs = " + attrs.getAttributeName(i));
}
}
//输出结果
attrs = layout_width
attrs = layout_height
attrs = src
发现了什么?这些值刚好是我在xml里引入时设置的初值!如果我在xml去掉src引用,打印显示的结果也会去掉。
由此可以推断,attrs代表的是已设置的属性集合。
看网上的自定义View,第一步就是在重写的构造函数里获取TypedArray属性,如下
public MyGifView(final Context context, final AttributeSet attrs) {
super(context, attrs);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.GifView);
}
就目前代码量而言,这句话会报错,因为根本没有R.styleable.GifView
原因:
首先这个方法 obtainStyledAttributes(AttributeSet set, int attrs[])
的含义大概是从设置的属性里获取自定义样式属性,第一个参数,刚刚验证了是xml里设置了的属性;第二个参数是我们自己自定义的所有属性集合。而现在我们并没有自定义属性,所以直接R.styleable是“点”不出来的。
自定义属性不难,就是在工程的res/values下新建attrs.xml,然后在里面加入如下标签组定义
<resources>
<declare-styleable name = "GifView">
<attr name = "gif_src" format = "reference"/>
</declare-styleable>
</resources>
可以看到这里就有关键字“styleable”的出现。
简单介绍一下,要注意的地方(自己可以自定义的地方),有三个地方。
1.declare-styleable name 代表的是自定义的属性组名
2.attr name 代表的是具体能在xml中定义的属性名
3.format 代表的是属性格式。
format的值可以有:
reference:参考某一资源ID
color:颜色值
boolean:布尔值
dimension:尺寸值。
float:浮点值。
integer:整型值。
string:字符串
fraction:百分数。
enum:枚举值
flag:位或运算注意:属性定义格式可以指定多种类型
比如:
<attr name = "background" format = "reference|color"/>
调用时既可以用drawable资源又可以直接用颜色
android:background = “@drawable/图片ID | #00FF00”
在根部结点加入自己的命名空间xmlns:my = "http://schemas.android.com/apk/res/com.azz.mygifview"
,其中“my”是自定义标签,可以修改,用my:xxxx = ""
的形式来定义自己的属性;“com.azz.mygifview”是程序包名,以AndroidManifest里的package值为准。
放置位置如下:main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/andorid"
xmlns:my="http://schemas.android.com/apk/res/com.azz.mygifview"
android:layout_width = "match_parent"
android:layout_height = "match_parent">
<com.azz.mygifview.MyGifView
android:layout_width = "wrap_content"
android:layout_height = "wrap_content"
my:gif_src = "@drawable/coffee"/>
</RelativeLayout>
好,现在回到开始获取TypedArray的地方。
通过查阅源码,我又查到两个方法:
int getIndexCount() //得到属性个数
String getText(int index) //得到对应下标的数据的字符串名称
什么意思呢?简单组合一下就知道答案了:
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.GifView); //获取已设置的自定义属性组
int count = typedArray.getIndexCount();
for (int i = 0; i < count; i++) {
Log.d(TAG, "typedArray = " + typedArray.getText(i));
}
typedArray.recycle(); //TypedArray是共享资源,用完一定要回收
//输出结果
typedArray = res/drawable-mdpi/coffee.gif
注意:TypedArray是共享资源,用完一定要用typedArray.recycle();
回收
如果自定义属性是color类型,xml设置属性值为#000000,打印出来的也是#000000。可以根据打印猜到些什么了,打印的是属性的值。
那么,如何获取这些属性的具体值呢?
TypedArray提供非常多的get方法,比如
boolean getBoolean(int index, boolean defValue) //获取boolean类型的值
float getFloat(int index, float defValue) //获取浮点类型的值
int getInt(int index, int defValue) //获取整数类型的值
String getString(int index) //获取字符串类型的值
int getResourcesId(int index, int defValue) //获取资源文件id
Drawable getDrawable(int index) //获取图片类型的值
defValue 看字面意思就知道是 default value-缺省值,当获取不到值时则返回缺省值。
index 是这个属性的索引,用R.styleable.XXX
获得属性索引,其实这个索引就是0、1、2…按着attrs.xml里面GifView组里面定义顺序排的(第一行定义的属性(比如gif_src)索引就是0,第二行1…),这个索引值当你保存xml文件的时候,自动在R文件中生成。
/** * GifView为属性组名,gif_src为具体属性名,他们之间用下划线连接得到索引名(eclipse“点”的时候会出来提示的),其实 * 点击进去发现,GifView_gif_src就等于0 */
int resId = typedArray.getResourceId(R.styleable.GifView_gif_src, 0);
上面是获取图片资源文件id,还可以直接获取图片Drawable类型,如下
Drawable drawable = typedArray.getDrawable(R.styleable.GifView_gif_src);
总体而言:我们需要借助 Android SDK 自带的 Movie 类进行播放 gif 动画帧。
之前我们已经获取到了图片的资源文件resId
,现在需要先把资源文件转换成movie
InputStream iStream = getResources().openRawResource(resId); //此方法能通过资源文件id查找到资源文件并转化为输入流
mMovie = Movie.decodeStream(iStream); //输入流转化为Movie (mMovie 为全局变量,类型 Movie)
因为gif_src是自定义属性,当宽高(layout_width | height)设置为wrap_content的时候,需要我们重新设定尺寸(重写onMeasure()),不然是不会显示出gif图片的。
所以紧接着获取图片宽高
if (mMovie != null) {
Bitmap bitmap = BitmapFactory.decodeStream(iStream); //将输入流转换为Bitmap
mWidth = bitmap.getWidth(); //宽,全局变量,int类型
mHeight = bitmap.getHeight(); //高,全局变量,int类型
bitmap.recycle(); //已经不需要图片资源了,释放它
}
接着重写onMeasure(),自定义view的尺寸
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (mMovie != null) {
setMeasuredDimension(mWidth, mHeight); //重新设定宽高
}
}
这个时候如果xml是这样写
<com.azz.mygifview.MyGifView
android:layout_width = "wrap_content"
android:layout_height = "wrap_content"
android:gif_src = "@drawable/coffee"
android:background = "#000000"/>
可以看到view是宽高等于gif图片的黑色区域
剩下最后一步,也是最重要的一步,重写onDraw来实现播放gif动图
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mMovie != null) {
playMovie(canvas); //播放gif
invalidate(); //刷新界面
}
}
/** * @Description 开始播放gif * @param canvas 画布 */
private void playMovie(Canvas canvas) {
if (mMovie == null) {
return false;
}
long now = SystemClock.uptimeMillis(); //得到当前时间
//第一次播放
if (mMovieStart == 0) { //gif播放开始时间,全局变量,long类型
mMovieStart = now; //记录播放开始时间
}
int duration = mMovie.duration(); //帧间隔时间
int relTime = (int)((now - mMovieStart) % duration); //计算出当前播放的时间点
mMovie.setTime(relTime); //设置播放时间点
mMovie.draw(canvas, 0, 0); //在画布上画出当前帧
}
注释代码里已经比较详细了。
这个时候再运行!~
很糟糕,view还是一片黑!但是我自己加了很多打印,能看出来是在循环播放的,只是不显示!
原来跟Android 4.0有关
有些4.0以上系统的手机启动了硬件加速功能之后会导致GIF动画播放不出来,因此我们需要在 AndroidManifest.xml 中去禁用硬件加速功能,可以通过指定
android:hardwareAccelerated
属性来完成
所以只要在 AndroidManifest.xml 中的<application>
标签或者<activity>
标签里加上android:hardwareAccelerated="false"
如下在 AndroidManifest.xml
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme"
android:hardwareAccelerated="false"
>
...
</application>
将<uses-sdk />
标签移到文件的最后就可以了。
如下在 AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.azz.mygifview" android:versionCode="1" android:versionName="1.0" >
<application>
<activity>
</activity>
</application>
<uses-sdk android:minSdkVersion="8" android:targetSdkVersion="18" />
</manifest>
两种方法都亲测有效!
现在再运行,gif终于出来了!
直到做完我才发现,原来是可乐,不是咖啡=。=
源码地址:https://github.com/Xieyupeng520/MyGifView_V1.0.git
本项目仍有一些不成熟的地方,比如
1.gif_src 属性只支持 gif 图,并不支持其他类型的图片
2.只支持默认的引用图片,不能另外设置
本项目实用性差,建议只做学习用。
下一篇《【Android实战】记录自学自定义GifView过程,能同时支持gif和其他图片!【实用篇】》会讲解如何解决这些问题!
References:
《Android PowerImageView实现,可以播放动画的强大ImageView》
《说说Android中的style和theme》
《[Android实例] 根据好些例子用movie类显示动的gif,但总是连图片都看不到》
本文有些关于理解自定义view属性不清楚的地方,可以看看《Android 深入理解Android中的自定义属性》
如果你有任何问题,欢迎留言告诉我!