Android 自定义属性与数据绑定(以一个自定义简单GIF控件为例)

写在最前面:这里所指的自定义GIF控件指的是一个继承自imageview的、使用帧动画将一系列图片播放出来的控件,并不是读取GIF图片播放的控件

一、实现内容

  1. 创建一个自定义控件,可以播放图片资源达成类似GIF的效果

  2. 在自定义控件中使用自定义属性

  3. 使用MVVM框架,在viewmodel中可以设置图片资源

二、学习目标

  • 学习如何设置自定义属性

  • 学习如何将自定义属性进行数据绑定

三、实现

1. 自定义控件

首先完成基本的内容,自定义GIF控件,主要使用到的就是帧动画

java 复制代码
public class GifView extends AppCompatImageView {

    private AnimationDrawable animationDrawable;

    public GifView(Context context) {
        super(context);
        init();
    }

    public GifView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {

        animationDrawable = new AnimationDrawable();
        setImageDrawable(animationDrawable);

        int[] gifResourceIds = {R.drawable.frame_1, R.drawable.frame_2, R.drawable.frame_3};
        setGifResourceIds(gifResourceIds,100);
        startGif();
    }

    public void setGifResourceIds(int[] resourceIds, int duration) {
        for (int resourceId : resourceIds) {
            animationDrawable.addFrame(getResources().getDrawable(resourceId), duration);
        }
    }

    public void startGif() {
        animationDrawable.start();
    }

    public void stopGif() {
        animationDrawable.stop();
    }
}

在xml文件中这样就可以使用这个自定义控件

xml 复制代码
<com.example.gif.GifView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    />

这是我最开始写的版本,仅仅完成了播放动画的要求,图片也是写死在里面的,当然我也可以选择在MainActivity使用findViewByID,再调用GifView中的setGifResourceIds去设置图片,但是如果我想用MVVM框架的话,这就是一个不合适的写法,所以我考虑使用自定义属性。

2. 自定义属性

  • 在res/values/attrs.xml中定义自定义属性:

创建一个XML文件,通常命名为attrs.xml,位于res/values/目录下。在这个文件中,你可以定义自己的属性。例如:

xml 复制代码
<resources>
    <declare-styleable name="GifView">
        <attr name="gifResourceIds" format="reference" />
        <attr name="gifDuration" format="integer" />
    </declare-styleable>
</resources>

这里我定义了两个属性,但是后面实际使用我只用了一个,如果大家有兴趣可以自己完成第二个自定义属性的设置。这里gifResourceIds我使用的format是reference,指向一个引用id

  • 在布局文件中使用自定义属性: 在布局文件中,使用自定义属性给相应的控件设置值。

首先增加一个新的XML命名空间声明,它告诉Android系统如何解释和处理XML布局文件中的自定义属性。

xml 复制代码
xmlns:app="http://schemas.android.com/apk/res-auto"

然后就可以使用这个属性

xml 复制代码
<com.example.gif.GifView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:gifResourceIds=""                 
    />

但是因为我们还并没有在自定义view中读取和使用这个属性,所以现在它是没有意义的。不过这里我遇到了一个新的问题,我这里想传入的属性值应该是一堆图片------就是我想让他们像GIF一样播放出来的一堆图片,所以我应该传入一个类似数组的东西。

在这里我的解决方案是,在xml中配置一个数组资源,其中包含我想要播放的图片。我之前有提到自定义属性的format是reference,所以我这里传入一个资源id就可以了。

配置数组资源

res/values/目录下创建一个XML文件(例如,arrays.xml):

xml 复制代码
<resources>

    <array name="gifResourceIds">
        <item>@drawable/frame_1</item>
        <item>@drawable/frame_2</item>
        <item>@drawable/frame_3</item>
    </array>
</resources>

这样我们就可通过它的资源id R.array.gifResourceIds来获取它了

  • 在自定义控件中读取和使用自定义属性:

现在回到之前的内容,在自定义控件的Java类中,我们需要读取和使用自定义属性的值,这样这个属性才有意义。在构造函数或初始化方法中,可以使用obtainStyledAttributes方法获取属性值。

java 复制代码
TypedArray ta=context.obtainStyledAttributes(attrs,R.styleable.GifView);
resData=ta.getResourceId(R.styleable.GifView_gifResourceIds,0);

这里获取该属性的资源ID,并将其存储在变量 resData

  • 解析数据

这里我们获取了这个数据,但是它是这个数组的资源id,我们需要的是数组内的图片资源,所以我们需要将其中的数据读取出来

java 复制代码
private void initRes() {
    // 获取引用的资源 ID 数组
    if (resData == Resources.ID_NULL) {
        return;
    }
    TypedArray resourceArray = getResources().obtainTypedArray(resData);
    int length = resourceArray.length();
    resourceIds.clear();
    for (int i = 0; i < length; i++) {
        resourceIds.add(resourceArray.getResourceId(i, 0));
    }
    resourceArray.recycle();
}
    

经过这里我们终于获得图片资源

3. GifView代码

这里先贴上全部源码

java 复制代码
public class GifView extends AppCompatImageView {
    private final String TAG= "GifView";
    private AnimationDrawable animationDrawable;
    private int resData;
    private ArrayList<Integer> resourceIds = new ArrayList<>();
    private ArrayList<Integer> temp;

    public GifView(Context context) {
        super(context);
        Log.d(TAG, "GifView: no param");
    }

    public GifView(Context context, AttributeSet attrs) {
        super(context, attrs);
        Log.d(TAG, "GifView: have param");
        TypedArray ta=context.obtainStyledAttributes(attrs,R.styleable.GifView);
        Log.d(TAG, "GifView: ta"+ta);
        Log.d(TAG, "GifView: ta.getIndexCount()"+ta.getIndexCount());
        resData=ta.getResourceId(R.styleable.GifView_gifResourceIds,0);
        
        initRes();
        init();
    }

    private void initRes() {
        // 获取引用的资源 ID 数组
        if (resData == Resources.ID_NULL) {
            Log.d(TAG, "initRes: resource id is null");
            return;
        }
        TypedArray resourceArray = getResources().obtainTypedArray(resData);
        int length = resourceArray.length();
        resourceIds.clear();
        for (int i = 0; i < length; i++) {
            resourceIds.add(resourceArray.getResourceId(i, 0));
        }
        resourceArray.recycle();
    }
    public void setGifResourceIds(int gifResourceIds) {
        resData= gifResourceIds;
        initRes();
        init();
    }

    private void init() {
        if (animationDrawable != null) {
            if (animationDrawable.isRunning()) {
                animationDrawable.stop();
            }
        }
        animationDrawable = new AnimationDrawable();
        setImageDrawable(animationDrawable);

        if (resourceIds != null && resourceIds.size() > 0) {
            for (int resourceId : resourceIds) {
                animationDrawable.addFrame(getResources().getDrawable(resourceId), 100);
            }
        }

        startGif();
    }

    public void startGif() {
        animationDrawable.start();
    }

    public void stopGif() {
        animationDrawable.stop();
    }

}

现在我们的自定义属性已经可以使用了,我们可以尝试一下

xml 复制代码
<LinearLayout
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.example.gifviewmvvm.GifView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:gifResourceIds="@array/gifResourceIds"
         />

</LinearLayout>

如果图片可以动了就成功了

4. Viewmodel

现在来写viewmodel,在我们这个viewmodel只需要实现一个简单的设置资源的功能

java 复制代码
public class GifViewModel extends ViewModel {

    private MutableLiveData<Integer> gifResourceIds = new MutableLiveData<>();

    public GifViewModel(){
        setGifResourceIds();
    }

    public void setGifResourceIds(){
        gifResourceIds.setValue(R.array.gifResourceIds);
    }

    public MutableLiveData<Integer> getGifResourceIds() {
        return gifResourceIds;
    }

}

5. MainActvity

java 复制代码
binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
viewModel = new ViewModelProvider(this).get(GifViewModel.class);
binding.setViewModel(viewModel);
binding.setLifecycleOwner(this);

现在所有内容都完成了!可以试试在xml使用双向数据绑定了

xml 复制代码
<com.example.gifviewmvvm.GifView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:gifResourceIds="@{viewModel.gifResourceIds}"
     />

可以尝试再在页面做一个按钮,实现点击按钮更换图片资源的效果,体会mvvm框架的便利。

四、总结

经过这次练习感觉对于MVVM框架更加清晰了一些,不过这个自定义控件是继承自imageview的,所以写起来并不需要measure,layout,draw的流程,降低了难度,以后可以试试更复杂的版本。

相关推荐
Kapaseker几秒前
你不看会后悔的2025年终总结
android·kotlin
alexhilton3 小时前
务实的模块化:连接模块(wiring modules)的妙用
android·kotlin·android jetpack
ji_shuke4 小时前
opencv-mobile 和 ncnn-android 环境配置
android·前端·javascript·人工智能·opencv
sunnyday04266 小时前
Spring Boot 项目中使用 Dynamic Datasource 实现多数据源管理
android·spring boot·后端
幽络源小助理7 小时前
下载安装AndroidStudio配置Gradle运行第一个kotlin程序
android·开发语言·kotlin
inBuilder低代码平台7 小时前
浅谈安卓Webview从初级到高级应用
android·java·webview
豌豆学姐7 小时前
Sora2 短剧视频创作中如何保持人物一致性?角色创建接口教程
android·java·aigc·php·音视频·uniapp
白熊小北极7 小时前
Android Jetpack Compose折叠屏感知与适配
android
HelloBan7 小时前
setHintTextColor不生效
android
洞窝技术10 小时前
从0到30+:智能家居配网协议融合的实战与思考
android