我现在要自定义一个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
构造方法参数列表里有个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_widthattrs = layout_heightattrs = src
发现了什么?这些值刚好是我在xml里引入时设置的初值!如果我在xml去掉src引用,打印显示的结果也会去掉。
由此可以推断,attrs代表的是已设置的属性集合。
TypedArray(1)
看网上的自定义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 = [email protected]/图片ID | #00FF00”
如何在xml中加上自定义属性
在根部结点加入自己的命名空间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(2)
好,现在回到开始获取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) //获取资源文件idDrawable getDrawable(int index) //获取图片类型的值
defValue 看字面意思就知道是 default value-缺省值,当获取不到值时则返回缺省值。
index 是这个属性的索引,用R.styleable.XXX
获得属性索引,其实这个索引就是0、1、2…按着attrs.xml里面GifView组里面定义顺序排的(第一行定义的属性(比如gif_src)索引就是0,第二行1…),这个索引值当你保存xml文件的时候,自动在R文件中生成。
int resId = typedArray.getResourceId(R.styleable.GifView_gif_src, 0); //GifView为属性组名,gif_src为具体属性名,他们之间用下划线连接得到索引名(eclipse“点”的时候会出来提示的),其实点击进去发现,GifView_gif_src就等于0
上面是获取图片资源文件id,还可以直接获取图片Drawable类型,如下
Drawable drawable = typedArray.getDrawable(R.styleable.GifView_gif_src);
开始自定义显示Gif
总体而言:我们需要借助 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图片的。
重定义宽高onMeasure
所以紧接着获取图片宽高
if (mMovie != null) { Bitmap bitmap = BitmapFactory.decodeStream(iStream); //将输入流转换为Bitmap mWidth = bitmap.getWidth(); //宽,全局变量,int类型 mHeight = bitmap.getHeight(); //高,全局变量,int类型 bitmap.recycle(); //已经不需要图片资源了,释放它}
接着重写onMeasure(),自定义view的尺寸
@Overrideprotected 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 = "#0000000"/>
可以看到view是宽高等于gif图片的黑色区域
重定义绘图onDraw
剩下最后一步,也是最重要的一步,重写onDraw来实现播放gif动图
@Overrideprotected 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); //在画布上画出当前帧}
注释代码里已经比较详细了。
这个时候再运行!~
可能出现的问题-黑屏、不显示gif
很糟糕,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
本项目仍有一些不成熟的地方,比如只支持gif,不支持其他格式图片,建议只做学习用。
下一篇《【Android实战】记录自学自定义GifView过程,能同时支持gif和其他图片!【实用篇】》会讲解如何完美解决该问题!
References:
《Android PowerImageView实现,可以播放动画的强大ImageView》
《说说Android中的style和theme》
《[Android实例] 根据好些例子用movie类显示动的gif,但总是连图片都看不到》
本文有些关于理解自定义view属性不清楚的地方,可以看看《Android 深入理解Android中的自定义属性》
如果你有任何问题,欢迎留言告诉我!
版权声明:本文为博主原创文章,未经博主允许不得转载。