【Android实战】记录自学自定义GifView过程,详解属性那些事!【学习篇】

我现在要自定义一个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的第一帧图片。
【Android实战】记录自学自定义GifView过程,详解属性那些事!【学习篇】_第1张图片

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_width
attrs = layout_height
attrs = 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 = “@drawable/图片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) //获取资源文件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); 

开始自定义显示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的尺寸

@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()

剩下最后一步,也是最重要的一步,重写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); //在画布上画出当前帧
}

注释代码里已经比较详细了。

这个时候再运行!~

可能出现的问题-黑屏、不显示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

本项目仍有一些不成熟的地方,比如

1.gif_src 属性只支持 gif 图,并不支持其他类型的图片

2.只支持默认的引用图片,不能另外设置

本项目实用性差,建议只做学习用。

下一篇《【Android实战】记录自学自定义GifView过程,能同时支持gif和其他图片!【实用篇】》会讲解如何解决这些问题!

References:
《Android PowerImageView实现,可以播放动画的强大ImageView》
《说说Android中的style和theme》
《[Android实例] 根据好些例子用movie类显示动的gif,但总是连图片都看不到》

本文有些关于理解自定义view属性不清楚的地方,可以看看《Android 深入理解Android中的自定义属性》

如果你有任何问题,欢迎留言告诉我!

你可能感兴趣的:(android,attribute,TypedArray,自定义view,attrs)