安卓开发学习之dataBinding的学习使用

背景

dataBinding数据绑定是谷歌推出的基于观察者模式的数据和页面内容的绑定,拥有广阔的应用前景。

使用

使能

dataBinding使能只需要在module的gradle文件里加上这么一段话

android {
    ...

    dataBinding {
        // 使能dataBinding
        enabled = true
    }

    ...
}

就可以了

创建数据实体

官方推荐的数据实体是javaBean,先让这个类继承自BaseObservable,然后在需要和布局内容绑定的属性的get方法前,加上@Bindable注解,以及set方法里,属性更改后,调用notifyPropertyChanged()方法。

ppackage applicationmanager.com.example.a123.studyofdatabinding.javaBean;

import android.databinding.BaseObservable;
import android.databinding.Bindable;

import applicationmanager.com.example.a123.studyofdatabinding.BR;

public class Person  extends BaseObservable {
    private String name;
    private int age;
    private String url;

    public Person(String name, int age, String url) {
        this.name = name;
        this.age = age;
        this.url = url;
    }

    @Bindable
    // 在需要观察的属性对应的get方法上加Bindable注解
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
        notifyPropertyChanged(BR.name);
        // BR相当于R,name是其属性对应的id。调用此方法进行实时显示
    }

    @Bindable
    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
        notifyPropertyChanged(BR.age);
    }

    @Bindable
    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
        notifyPropertyChanged(BR.url);
    }

    @Override
    public String toString() {
        final StringBuffer sb = new StringBuffer("Person{");
        sb.append("name='").append(name).append('\'');
        sb.append(", age=").append(age);
        sb.append(", url='").append(url).append('\'');
        sb.append('}');
        return sb.toString();
    }
}

这样,一个可观察的javaBean就创建完了

布局内容

最大的变化感觉还是在布局方面,dataBinding把原来的根标签换成了<layout>标签,下面有<data>和真正的根布局。

因此,要把需要绑定的数据声明到<data>数据域里

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <!-- 根节点换成layout -->

    <!-- 数据区包裹在<data>标签下 -->
    <data>

        <variable
            name="person"
            type="applicationmanager.com.example.a123.studyofdatabinding.javaBean.Person"/>
        <!-- 声明要用到的变量,名字和类型 -->
        
    </data>

    <!-- 原来的布局 -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context=".MainActivity">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{person.name + @string/separator + person.age}" />
        <!-- 获取属性 -->

        <Button
            android:id="@+id/increment"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="点击改变"
            />

    </LinearLayout>
</layout>

数据域里的变量要冠以variable标签,里面写上变量的名字和类型。而后在布局里用的时候用@{}来对变量进行引用。引用时,{}里不能直接出现要显示的字符串,如果要显示固定的文字,必须在资源文件(res/values/strings.xml)里定义,比如这样

<resources>
    <string name="app_name">StudyOfDataBinding</string>
    <string name="separator">-------</string>
</resources>

为了展示数据的实时变化,我加了一个按钮,点击一下person的年龄+1

Activity里进行绑定

方才是设计了一个javaBean实体,而后在布局文件里声明了一个变量,下面就是把它们俩绑定到一起的过程

package applicationmanager.com.example.a123.studyofdatabinding;

import android.databinding.ViewDataBinding;
import android.os.Bundle;

import applicationmanager.com.example.a123.studyofdatabinding.javaBean.Person;

public class MainActivity extends Activity {
 
    private ViewDataBinding mBinding;
    private Person person;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        // 加载绑定布局

        person = new Person("Jason", 24, "http://p0.so.qhmsg.com/bdr/_240_/t01825773612648be9f.jpg");
        mBinding.setVariable(BR.person, person);

        findViewById(R.id.increment).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                person.setAge(person.getAge() + 1);
            }
        });

    }

}

步骤如上所示,利用DataBindingUtil的setContentView替换原来的setContentView,来把当前Activity和指定的布局文件绑定在一起,同时获取一个ViewDataBinding对象,而后利用此对象,把布局里的person变量和activity里的person对象绑定在一起。

运行之后,效果如图所示

这样,最基本的数据绑定就实现了。

ObservableInt等

如果需要使用一个可观察的基本数据类型或内置对象,可以使用DataBinding里的ObservableInt、ObservableString、ObservableArrayMap等,使用方法和绑定javaBean相似,但亦有不同。

布局文件里声明

这时声明的话需要在数据域里导包,因为这些类不在我们的工程里

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <!-- 根节点换成layout -->

    <!-- 数据区包裹在<data>标签下 -->
    <data>
        <import type="android.databinding.ObservableArrayMap"/>
        <import type="android.databinding.ObservableInt"/>
        <import type="android.databinding.ObservableField"/>
        <import type="android.databinding.ObservableArrayList"/>
        <!-- 导包 -->

        <variable
            name="person"
            type="applicationmanager.com.example.a123.studyofdatabinding.javaBean.Person"/>
        <!-- 声明要用到的变量,名字和类型 -->
        <variable
            name="count"
            type="ObservableInt"/>
        <variable
            name="info"
            type="ObservableField&lt;String&gt;"/>
        <!-- <>括号要转义,<改成&lt; >改成&gt; -->
        <variable
            name="maps"
            type="ObservableArrayMap&lt;String, String&gt;"/>
        <variable
            name="arrays"
            type="android.databinding.ObservableArrayList&lt;String&gt;"/>
        <variable
            name="index"
            type="ObservableInt"/>
        
    </data>

    <!-- 原来的布局 -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context=".MainActivity">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{person.name + @string/separator + person.age}" />
        <!-- 获取属性 -->

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{String.valueOf((Integer)count)}"/>
        <!-- 整数必须先转成Integer,再转成String -->

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{info}"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text='@{maps["name"]}'/>
        <!-- 根据键取值 -->

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text='@{maps["description"]}'/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{arrays[index]}"/>
        <!-- 遍历数组 -->
        <ListView
            android:id="@+id/list_people"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>

        <Button
            android:id="@+id/increment"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="点击改变"
            />
        <!-- 绑定监听,自动传入view -->
        <Button
            android:id="@+id/decrement"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="点击减少"
            />
    </LinearLayout>
</layout>

其中映射根据键取值,很像python里的操作,仿佛是在操作数组,此刻为了区分,我们可以给键加上双引号,@{}外面换成单引号。

再者,就是<>的转义了,把<改成&lt; >改成&gt;才能编译通过。

最后,引用时,要把ObservableInt转成字符串,就像上面的

android:text="@{String.valueOf((Integer)count)}"

activity里绑定

为了实现可观察数组和映射,我事先定义了一组数据

    private String[] names = new String[]{
            "A", "B", "C", "D"
    };
    private String[] descriptions = new String[]{
      "aa", "bb", "cc", "dd"
    };

然后在定义一堆Observable对象,其中最后的indexObservable是数组元素的下标

    private ObservableInt countObservable = new ObservableInt();
    private ObservableField<String> infoObservable = new ObservableField<>();
    private ObservableArrayMap<String, String> mapObservable = new ObservableArrayMap<>();
    private ObservableArrayList<String> arraysObservable = new ObservableArrayList<>();
    private ObservableInt indexObservable = new ObservableInt();

在onCreate()和布局里的变量的id进行绑定

        mBinding.setVariable(applicationmanager.com.example.a123.studyofdatabinding.BR.person, person);
        mBinding.setVariable(applicationmanager.com.example.a123.studyofdatabinding.BR.count, countObservable);
        mBinding.setVariable(applicationmanager.com.example.a123.studyofdatabinding.BR.info, infoObservable);
        mBinding.setVariable(applicationmanager.com.example.a123.studyofdatabinding.BR.maps, mapObservable);
        mBinding.setVariable(applicationmanager.com.example.a123.studyofdatabinding.BR.arrays, arraysObservable);
        mBinding.setVariable(BR.index, indexObservable);

而后,开启两个线程,进行数据的改变。为了实现数组的自行遍历,单门为index开了一个线程。

对于Observable对象,基本就是set设值,get取值。对于映射和数组,set/get方法和普通的映射数组一样,其中数组也可以给指定下标的元素设值。

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    String name = names[i % names.length];
                    String decription = descriptions[i % descriptions.length];

                    countObservable.set(i + 1); // set方法设值
                    infoObservable.set("当前计数器的值是:" + (i + 1) + ";线程名:" + Thread.currentThread().getName());
                    mapObservable.put("name", name);
                    mapObservable.put("description", decription);
                    arraysObservable.add(name);
//                    arraysObservable.set(0, name);

                    try {
                        Thread.sleep(3 * 1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    indexObservable.set(i);
                    try {
                        Thread.sleep(3 * 1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();

运行效果如图所示

时间间隔有点长,但可以看到实现了数据的自行更新。

ListView的数据适配

其实要想显示数据列表的话,还是推荐用ListView而不是在布局里声明一个数组,毕竟ListView是单门用来显示列表的

main布局里声明

在activity布局里添加一个ListView

        <ListView
            android:id="@+id/list_people"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>

创建列表项的布局文件

然后给列表每一项创建通用的布局文件,这个布局文件要采用dataBinding格式

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <data>
        <variable
            name="personItem"
            type="applicationmanager.com.example.a123.studyofdatabinding.javaBean.Person"/>
    </data>
    <LinearLayout
        android:orientation="horizontal" android:layout_width="match_parent"
        android:layout_height="match_parent">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{@string/personName + personItem.name}"/>
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{@string/separator + @string/personAge + String.valueOf(personItem.age)}"/>
        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            app:imageUrl = "@{personItem.url}"
            android:layout_marginLeft="40dp"/>
    </LinearLayout>
</layout>

就是从左往右显示名字、年龄和图片。对于图片,我也是利用dataBinding的方法来实现加载的,一会儿再说它。

创建适配器

ListView显示数据当然要创建适配器

package applicationmanager.com.example.a123.studyofdatabinding.adapter;

import android.content.Context;
import android.databinding.DataBindingUtil;
import android.databinding.ViewDataBinding;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;

import java.util.LinkedList;

import applicationmanager.com.example.a123.studyofdatabinding.BR;
import applicationmanager.com.example.a123.studyofdatabinding.R;
import applicationmanager.com.example.a123.studyofdatabinding.javaBean.Person;

public class MyListViewBindingAdapter extends BaseAdapter {
    private LinkedList<Person> people;
    private Context mContext;

    public MyListViewBindingAdapter(LinkedList<Person> people, Context mContext) {
        this.people = people;
        this.mContext = mContext;
    }

    @Override
    public int getCount() {
        return people.size();
    }

    @Override
    public Object getItem(int i) {
        return people.get(i);
    }

    @Override
    public long getItemId(int i) {
        return i;
    }

    @Override
    public View getView(int i, View view, ViewGroup viewGroup) {
        ViewDataBinding binding = null;
        if (view == null) {
            Person person = people.get(i);
            if (person != null) {
                binding = DataBindingUtil.inflate(LayoutInflater.from(mContext), R.layout.list_item, viewGroup, false);
                binding.setVariable(BR.personItem, person);
            }
        } else {
            binding = DataBindingUtil.getBinding(view);
            // return view;
        }

        return binding.getRoot();
    }
}

和传统适配器唯一的区别来自getView()方法。

如果是第一次显示这一项,入参view必然是空,那么我们就获取people列表里第i个person,加载列表项的布局文件,获取ViewDataBinding对象,并利用此对象把person对象和布局中的personItem绑定起来。

如果不是第一次显示这一项,其实可以直接返回view了,因为此时view肯定不是空。但如果要对view数据改变绑定的话,可以通过DataBindingUtil的getBinding()方法,获取view的数据绑定对象,然后重新setVariable。

最后返回binding.getRoot(),也就是绑定的view。

但是在第一次显示的时候,不要直接返回view,因为DataBindingUtil.inflate()方法并没有给view赋值,view还是null

Activity里初始化ListView

初始化很常规

package applicationmanager.com.example.a123.studyofdatabinding;

import android.app.Activity;

import android.databinding.ViewDataBinding;
import android.os.Bundle;

import android.widget.ListView;

import applicationmanager.com.example.a123.studyofdatabinding.adapter.MyListViewBindingAdapter;

public class MainActivity extends Activity {
   
    private ViewDataBinding mBinding;

    private ListView mListView;
    private MyListViewBindingAdapter mAdapter;
    private LinkedList<Person> mPeole = new LinkedList<>();

    private String[] names = new String[]{
            "A", "B", "C", "D"
    };


    private String[] urls = new String[]{
        "http://p5.so.qhimgs1.com/bdr/_240_/t0158432ac9d02c74bb.jpg",
        "http://p4.so.qhmsg.com/bdr/_240_/t015300f4f65dc07f89.jpg",
        "http://p3.so.qhimgs1.com/bdr/_240_/t014bbc3f5ab3755383.jpg",
        "http://p0.so.qhimgs1.com/bdr/_240_/t017ee2f5c7e5fcef90.jpg",
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...

        mListView = findViewById(R.id.list_people);
        mAdapter = new MyListViewBindingAdapter(mPeole, this);
        mListView.setAdapter(mAdapter);

        for (int i = 0; i < 10; i++) {
            String name = names[i % names.length];
            int age = i + 1;
            String url = urls[i % urls.length];
            mPeole.add(new Person(name, age, url));
        }

        ...
    }
}

运行的结果如图所示

可见ListView的适配也完成了。下面,我们既就看看dataBinding是怎么加载图片的

图片的加载

以上面的ListView中的图片为例,我们在布局文件里自定义图片加载的方法app:imageUrl

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            app:imageUrl = "@{personItem.url}"
            android:layout_marginLeft="40dp"/>

为了实现这个app:imageUrl,我们再定义一个Monitor类,里面进行数据绑定方面的调度。

package applicationmanager.com.example.a123.studyofdatabinding.monitor;

import android.databinding.BindingAdapter;
import android.net.Uri;
import android.view.View;

import com.bumptech.glide.Glide;

import applicationmanager.com.example.a123.studyofdatabinding.R;
import applicationmanager.com.example.a123.studyofdatabinding.myInterface.IActivity;

public class Monitor {
    private IActivity mActivity;


    public Monitor(IActivity mActivity) {
        this.mActivity = mActivity;
    }


    // 绑定方法,对应app:imageUrl
    @BindingAdapter("imageUrl")
    public static void loadImage(ImageView view, String url) {
        Glide.with(view.getContext().getApplicationContext()).load(Uri.parse(url)).into(view);
    }
}

通过@BindingAdapter注解,就实现了布局文件里的方法和java里的方法的绑定,参数都是自定义的,但要注意,返回值最好是void,而且参数要合理,比如设置imageView的url,入参定义为view和url就可以了,这样也有利于高内聚。

另外,java方法一定要是静态的,如果不设置成静态的,也可以,这样就要利用dataBinding的component来实现,感觉component用处不是很大,而且不如static灵活,特别是要调用recreate()重新构造activity实现数据的变化,让我觉得不是太好用,这里我贴出一篇关于使用component的博文,供大家参考,链接

这样,我们就实现了图片的加载。如果要改变图片,只需要在MainActivity里设置people列表里的某一项的url即可。

方法的绑定

方才,我们看到了@BindingAdapter注解,可以实现布局文件里的方法和java方法的绑定。说到方法的绑定,最常用的是布局属性的统一更改或点击事件的统一调度。

布局属性的更改

这一点和方才的imageUrl类似,只不过注解传参要传指定的属性名字比如android:layout_marginLeft。这样只要在布局文件里设置了android:layout_marginLeft属性,就会调到我们的setLeftMargin()方法里,

    // 安卓属性setter
    @BindingAdapter("android:layout_marginLeft")
    public static void setLeftMargin(View view, int margin) {
        ViewGroup.LayoutParams layoutParams= view.getLayoutParams();
        if (layoutParams instanceof LinearLayout.LayoutParams) {
            ((LinearLayout.LayoutParams) layoutParams).leftMargin = margin + 5;
        } else if (layoutParams instanceof FrameLayout.LayoutParams) {
            ((FrameLayout.LayoutParams) layoutParams).leftMargin = margin + 5;
        } else if (layoutParams instanceof RelativeLayout.LayoutParams) {
            ((RelativeLayout.LayoutParams) layoutParams).leftMargin = margin + 5;
        }

        view.setLayoutParams(layoutParams);
    }

例子里,只是让左外间距多了5像素

点击事件的统一调度

这个需要在布局文件里传入monitor对象

        <variable
            name="monitor"
            type="applicationmanager.com.example.a123.studyofdatabinding.monitor.Monitor"/>

然后在View(比如Button)的onClick里,传入monitor的onClick

        <Button
            android:id="@+id/increment"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="点击改变"
            android:onClick="@{monitor::onClick}"
            />
        <!-- 绑定监听,自动传入view -->
        <Button
            android:id="@+id/decrement"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="点击减少"
            android:onClick="@{monitor::onClick}"/>

注意引用方法时要用双冒号

最后我们就实现monitor的onClick,注意,此时不用加注解

    public void onClick(View view) {
        if (mActivity == null) {
            return;
        }

        int old = mActivity.getData();
        switch(view.getId()){
            case R.id.increment:
                old++;
                break;
            case R.id.decrement:
                old--;
                break;
        }
        mActivity.onDataChanged(old);
    }

这里,我用了MVP模式在实现数据的更新。mActivity是接口对象,实现在Activity里

    @Override
    public void onDataChanged(int data) {
        mPeole.get(0).setAge(data);
    }

    @Override
    public int getData() {
        return mPeole.get(0).getAge();
    }

运行效果如图所示,注意看listView第一项的年龄变动

结语

dataBinding的常用用法就如上所示了,还有一些类型强转,布局里的运算符什么的,我不是很推荐使用,首先是AS对于xml文件的调试定位能力有限,再者我觉得还是让布局文件只处理数据显示的好,要把逻辑处理什么的放在java代码里比较合适。

猜你喜欢

转载自blog.csdn.net/qq_37475168/article/details/85246039