MVVM之前曾写过MVP、MVC的文章,之后一直忙别的,把MVVM的文章给忘了。由于之前项目的图标已经没有了,只能换个界面,实现的功能与前面MVC、MVP项目的一样。
3.MVVM
MVVM其实跟前面讲的MVP差不多,如图所示:
MVP
屏幕快照 2017-07-29 上午9.55.40.pngMVVM
屏幕快照 2017-07-29 上午10.05.03.png最大的区别就是DataBinding,先来看看各层的功能:
V层:.xml、Activity、Fragment,负责显示控件,通常还在Activity或者Fragment中获取ViewDataBinding的实例,将ViewModel的实例与这个实例绑定;
M层:与MVP的M层一样,存储数据、负责数据的业务逻辑,如网络请求、访问数据库,通常细分为model(数据的业务处理,如下面例子中获取经纬度和时间的逻辑代码部分)、bean(存储数据,如下面例子中的LocationEntity,存储经度、纬度、时间)
MV层:全称是ViewModel,类似于MVP的P层,但是通过DataBinding将V层与MV层绑定后,可以在MV层获取用户输入的数据和用户指令,调用M层获取数据,但是,当数据更新时,由于DataBinding,不需要在代码中对界面进行更新,这就是DataBinding的优势,ViewModel获取新数据后,V层可以自动更新
可以说DataBinding是MVVM的核心,怎么灵活利用DataBinding就很重要了,先上代码实现MVVM的设计模式,然后再来讲DataBinding的灵活利用。
功能:点击“查询”按钮显示当前经纬度和时间,当位置变化时自动刷新界面
Screenshot_20170729-114024.png Screenshot_20170729-114032.png Screenshot_20170729-114049.png代码结构:
屏幕快照 2017-07-29 上午11.29.01.png在build.gradle(Module:app)中开启dataBinding(android{}内部)
dataBinding{
enabled=true
}
屏幕快照 2017-07-29 上午10.21.28.png
manifests中声明以下三个权限
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
界面:
屏幕快照 2017-07-29 上午10.40.27.png
activity_main.xml(使用较新的ConstraintLayout)
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="viewModel"
type="com.example.lingo.mvvmdemo.viewModel.MainActivityViewModel"/>
</data>
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="当前经纬度"
android:textSize="25dp"
android:layout_marginLeft="8dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
android:layout_marginRight="8dp"
app:layout_constraintRight_toRightOf="parent"
android:layout_marginTop="8dp"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginBottom="8dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp" />
<TextView
android:id="@+id/latitude"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{viewModel.location[`latitude`]}"
tools:text="纬度"
android:textAllCaps="false"
android:layout_marginLeft="8dp"
app:layout_constraintLeft_toLeftOf="parent"
android:layout_marginRight="8dp"
app:layout_constraintRight_toRightOf="parent"
android:layout_marginTop="24dp"
app:layout_constraintTop_toBottomOf="@+id/textView"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp" />
<TextView
android:id="@+id/longitude"
android:textAllCaps="false"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{viewModel.location[`longitude`]}"
tools:text="经度"
android:layout_marginLeft="8dp"
app:layout_constraintLeft_toLeftOf="parent"
android:layout_marginRight="8dp"
app:layout_constraintRight_toRightOf="parent"
android:layout_marginTop="24dp"
app:layout_constraintTop_toBottomOf="@+id/latitude"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp" />
<TextView
android:id="@+id/date"
android:textAllCaps="false"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{viewModel.location[`date`],default=`haha`}"
tools:text="时间"
android:layout_marginRight="8dp"
app:layout_constraintRight_toRightOf="parent"
android:layout_marginLeft="8dp"
app:layout_constraintLeft_toLeftOf="parent"
android:layout_marginTop="24dp"
app:layout_constraintTop_toBottomOf="@+id/longitude"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp" />
<Button
android:background="@{1 < 3? @color/red : @color/white}"
android:id="@+id/search"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="查询"
android:onClick="@{(v)->viewModel.search(100)}"
android:layout_marginLeft="8dp"
app:layout_constraintLeft_toLeftOf="parent"
android:layout_marginRight="8dp"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginBottom="8dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp" />
</android.support.constraint.ConstraintLayout>
</layout>
可以看到.xml与以往不一样,这是因为要支持DataBinding做出的改变,最外面嵌套了<layout>标签,在真正的布局之前添加了<data>标签作为这个布局的数据域,<data>里面用<variable>声明的变量可以在控件中使用,例如:
android:text="@{viewModel.location['date']}"
绑定变量viewModel名为location的域中key为“date”的value,location是ArrayMap<K, V>子类的一个实例,同时实现了观察者模式,也就是说数据变化(key为“date”的value发生改变)会引起绑定界面的变化(android:text内容的变化)
android:onClick="@{viewModel::search}"
事件处理的绑定,编译器会给这个View(这里是一个Button)注册点击监听器,当点击后调用变量viewModel的search(View v)方法。这里search的声明除了方法名其余必须与View.OnClickListener接口的抽象函数public void onClick(View v) 一致,如果不一致编译期间会报错。
MainActivity.java
package com.example.lingo.mvvmdemo.view;
import android.content.pm.PackageManager;
import android.databinding.DataBindingUtil;
import android.support.annotation.NonNull;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.Toast;
import com.example.lingo.mvvmdemo.R;
import com.example.lingo.mvvmdemo.databinding.ActivityMainBinding;
import com.example.lingo.mvvmdemo.util.AppConfig;
import com.example.lingo.mvvmdemo.viewModel.MainActivityViewModel;
public class MainActivity extends AppCompatActivity {
private MainActivityViewModel viewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding binding=DataBindingUtil.setContentView(this, R.layout.activity_main);
viewModel=new MainActivityViewModel(MainActivity.this);
binding.setViewModel(viewModel);
}
public void showToast(String text) {
Toast.makeText(MainActivity.this,text,Toast.LENGTH_SHORT).show();
}
//6.0以后的系统请求权限的回调函数,无论哪种框架,这个方法只能在Activity中重载
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if(requestCode== AppConfig.REQUEST_CODE) {
for(int i=0;i<permissions.length;i++){
if(grantResults[i]== PackageManager.PERMISSION_GRANTED){
showToast("权限已被允许");
}else{
showToast("你拒绝了位置权限的申请");
}
}
}
}
}
屏幕快照 2017-07-29 下午2.56.29.pngActivityMainBinding binding=DataBindingUtil.setContentView(this, R.layout.activity_main);
右边👉的表达式既给Activity设置了界面,同时也返回ViewDataBinding子类的实例,这里ActivityMainBinding与activity_main.xml存在联系,其实就是为带有<data>标签的activity_main生成一个类,这个类是ViewDataBinding的子类,名字就是将.xml每个被分隔的单词的首字母大写,后面加上Binding
在ActivityMainBinding.class(当然你得先编译)右键-Go To-Declaration,进入到ActivityMainBinding.java文件
看看生成的ActivityMainBinding的源码部分:
那一堆的public final看上去是不是有点眼熟,认真看不就是activity_main.xml布局中的控件和它们的android:id吗?现在你的代码中已经不需要findViewById了,取代它的是binding.date,binding.image等等就可以对相应的控件进行操作了,例如:
binding.date="2017年7月29号"
等价于
TextView date=(TextView).findViewById(R.id.date);
date.setText("2017年7月29号");
为binding绑定ViewModel实例:
viewModel=new MainActivityViewModel(MainActivity.this);
binding.setViewModel(viewModel);
为了更好理解,同样上源码(ActivityMainViewModel.java):
屏幕快照 2017-07-29 下午3.05.12.png然后看回acitivity_mian的<variable>
屏幕快照 2017-07-29 下午3.07.13.png所有声明的variable都会生成一个对应的setter和一个getter,例如:
<variable
name="str"
type="String"/>
这里的str就是ViewModel(String类型)的名字,上面的viewModel也是ViewModel(MainActivityViewModel类型)取的一个名字,ViewModel可以是任意类型,作为MV层与V层绑定,这段代码将会在生成的ViewBinding的子类中有个void setStr(String)方法和String getStr()方法,调用对应的setter方法给binding绑定相应的ViewModel,实现V层与VM层的DataBinding
AppConfig.java
public class AppConfig {
public static final int REQUEST_CODE=1;
}
LocationModel.java(与MVP一样,除了类型、接口等名字,其他没有变化)
package com.example.lingo.mvvmdemo.model;
import android.content.Context;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import com.example.lingo.mvvmdemo.bean.LocationEntity;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
/**
* Created by lingo on 2017/7/29.
*/
public class LocationModel {
private Context mContext;
private LocationEntity mLocationEntity;//持有bean层的对象
private LocationManager locationManager;
private LocationListener locationListener;
public LocationModel(Context mContext){
this.mContext=mContext;
mLocationEntity=new LocationEntity();
}
//请求位置数据的业务处理,部分代码可跳过,只需明白结构
public void requestLocate(final OnLocationModelListener listener) {
locationManager=(LocationManager)((mContext).
getSystemService(Context.LOCATION_SERVICE));
String locationProvider;
List<String> providers = locationManager.getProviders(true);
if(providers.contains(LocationManager.NETWORK_PROVIDER)){
//如果是Network
locationProvider = LocationManager.NETWORK_PROVIDER;
} else if(providers.contains(LocationManager.GPS_PROVIDER)){
//如果是GPS
locationProvider = LocationManager.GPS_PROVIDER;
}else{
listener.fail(01,"没有可用的位置提供器");
return ;
}
locationListener = new LocationListener() {
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
}
@Override
public void onProviderEnabled(String provider) {
}
@Override
public void onProviderDisabled(String provider) {
}
//当坐标改变时触发此函数,如果Provider传进相同的坐标,它就不会被触发
@Override
public void onLocationChanged(Location location) {
if (location != null) {
String latitude;
if(location.getLatitude()>0){
latitude="N"+String.valueOf(location.getLatitude());
}else{
latitude="S"+String.valueOf(-location.getLatitude());
}
String longitude;
if(location.getLongitude()>0){
longitude="E"+String.valueOf(location.getLongitude());
}else{
longitude="W"+String.valueOf(-location.getLongitude());
}
mLocationEntity.setLatitude(latitude);
mLocationEntity.setLongitude(longitude);
SimpleDateFormat formatter = new SimpleDateFormat("yy年MM月dd日 HH:mm:ss");
Date date=new Date(System.currentTimeMillis());
mLocationEntity.setDate(formatter.format(date));
listener.success(mLocationEntity);
}
}
};
//获取Location后将数据存储在 mLocationEntity(bean层),回调调用 listener.success(mLocationEntity);
try{
Location location = locationManager.getLastKnownLocation(locationProvider);
if (location != null) {
String latitude;
if(location.getLatitude()>0){
latitude="N"+String.valueOf(location.getLatitude());
}else{
latitude="S"+String.valueOf(-location.getLatitude());
}
String longitude;
if(location.getLongitude()>0){
longitude="E"+String.valueOf(location.getLongitude());
}else{
longitude="W"+String.valueOf(-location.getLongitude());
}
mLocationEntity.setLatitude(latitude);
mLocationEntity.setLongitude(longitude);
SimpleDateFormat formatter = new SimpleDateFormat("yy年MM月dd日 HH:mm:ss");
Date date=new Date(System.currentTimeMillis());
mLocationEntity.setDate(formatter.format(date));
listener.success(mLocationEntity);//回调
}
//监视地理位置变化
locationManager.requestLocationUpdates(locationProvider, 3000, 1, locationListener);
}catch (SecurityException e){
listener.fail(02,e.getMessage());
}catch(IllegalArgumentException e){
listener.fail(03,e.getMessage());
}
}
public interface OnLocationModelListener {
void success(Object oj);
void fail(int code,String message);
}
}
LocationEntity.java
package com.example.lingo.mvvmdemo.bean;
/**
* Created by lingo on 2017/7/29.
*/
public class LocationEntity {
private String latitude;
private String longitude;
private String date;
public String getDate(){
return date;
}
public void setDate(String date){
this.date=date;
}
public String getLatitude() {
return latitude;
}
public void setLatitude(String latitude) {
this.latitude = latitude;
}
public String getLongitude() {
return longitude;
}
public void setLongitude(String longitude) {
this.longitude = longitude;
}
}
MainActivityViewModel.java
package com.example.lingo.mvvmdemo.viewModel;
import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.databinding.ObservableArrayMap;
import android.os.Build;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.view.View;
import android.widget.Toast;
import com.example.lingo.mvvmdemo.bean.LocationEntity;
import com.example.lingo.mvvmdemo.model.LocationModel;
import com.example.lingo.mvvmdemo.util.AppConfig;
import com.example.lingo.mvvmdemo.view.MainActivity;
/**
* Created by lingo on 2017/7/28.
*/
public class MainActivityViewModel {
public ObservableArrayMap<String, Object> location = new ObservableArrayMap<>();
private Context mContext;//将引用MainActivity实例
private LocationModel mLocationModel;//持有M层实例
public MainActivityViewModel(MainActivity mContext) {
this.mContext=mContext;
this.mLocationModel = new LocationModel(mContext.getApplicationContext());//为了与V层解耦,传入
location.put("latitude","纬度");
location.put("longitude","经度");
location.put("date","时间");
//Application的Context实例
}
//当用户点击“查询”按钮时,该方法被调用
//这一部分主要是权限的处理,6.0是一个分界点
//6.0以前的权限处理和6.0以后的不一样,两种情况均要考虑,权限处理的细节
public void search(View v) {
int sdkInt = Build.VERSION.SDK_INT;
if (sdkInt < Build.VERSION_CODES.M) {
getLocate();
return;
}
int permission = ContextCompat.checkSelfPermission
(mContext, Manifest.permission.ACCESS_FINE_LOCATION);
if (permission != PackageManager.PERMISSION_GRANTED) {//没有开启权限
ActivityCompat.requestPermissions((MainActivity) mContext,
new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
AppConfig.REQUEST_CODE);//会在MainActivity中回调
}else{
getLocate();
}
}
//调用M层加载数据,在回调的success方法中更新location(ObservableArrayMap<String, Object>),
//activity_main.xml中控件的属性绑定了ObservableArrayMap的元素,而且ObservableArrayMap实现了观察者模式,
//所以只要location一改变,界面就会自动改变,不用在添加更新UI的代码
public void getLocate() {
mLocationModel.requestLocate(new LocationModel.OnLocationModelListener() {
@Override
public void success(Object oj) {
LocationEntity mLocationEntity=(LocationEntity)oj;
location.put("latitude",mLocationEntity.getLatitude());
location.put("longitude",mLocationEntity.getLongitude());
location.put("date",mLocationEntity.getDate());
}
@Override
public void fail(int code, String message) {
showToast("错误代码:"+String.valueOf(code)+"错误信息:"+message);
}
});
}
public void showToast(String text) {
Toast.makeText(mContext.getApplicationContext(),text,Toast.LENGTH_SHORT).show();
}
}
到这里你应该清楚的意识到了MVP与MVVM的最大区别:VM层(相当于P层)获取到M层回调的数据后不需要添加更新UI的代码(MVP则需要),这是通过DataBinding将V层与MV层绑定,让V层控件与MV层数据(这些数据从M层获取)绑定,当MV层的这些数据通过调用M层得到更新时,V层自动更新控件
DataBinding灵活应用
你以为这样就完了吗?这样你就对MVVM满足了?MVVM的关键在于DataBinding,谷歌支持的DataBinding还有更多接下来要解锁的功能,灵活应用DataBinding能让你的代码更加简洁、更加爱MVVM。
控件属性绑定VM层的域或者getXX方法
当然这个VM层要申明在.xml的<data>标签中,如:
<variable
name="viewModel"
type="com.example.lingo.mvvmdemo.viewModel.MainActivityViewModel"/>
其实有一个默认的context变量没有显示声明,其实就是 rootView 的 getContext()方法的返回值,可以在@{}这样的binding表达式中使用
通常,我会在VM层对应的xxViewModel.java文件中添加要绑定的成员域,当然这些域的值还是要调用M层来更新,或者只添加对应的getXX方法。
例如.xml中的一个TextView控件
android:text="@{viewModel.firstName}"
当然也可以做一些操作,如android:text="@{viewModel.firstName+String.valueOf(1)}"或者@{viewModel.firstName+'1'}
更多的操作如三元运算等如下:
屏幕快照 2017-07-29 下午5.05.48.png资源等也是支持的:
android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"
那么我的xxViewModel.java文件中将有以下任何一种形式:
1.public String firstName;
2.private String firstName;//也可以不写
public String getFirstName(){}
3.public String firstName(){}//比较少
需要说明的是通常调用M层后,M层会回调MV层的一个方法,如上面代码中的onSuccess或者onFail方法,在onSuccess中调用setFirstName(更新后的值)或者使用表达式fisrtName=更新后的值,从而使得MV层与V层绑定的数据得到更新,然而采用上面任何一种形式都不会自动更新UI,因为没有实现观察者模式,那么怎么实现观察者模式呢?
你是否注意到了上面项目中的public ObservableArrayMap<String, Object> location,没错,这个类其实是实现了观察者模式的,所以当location的元素变化时对应控件会自动更新。但素,不是每个MV层的成员域都要求是集合,对于像String
firstName这样的成员域要怎么实现观察者模式以自动更新UI呢?
对于成员域,你可以这样:
public ObservableField<String> firstName =
new ObservableField<>();
怎么操作这个firstName呢?
使用ObservableField<T>的成员方法T get和set(T)
firstName.set("haha");
String name=firstName.get();
如果是基本的数据类型,安卓还提供了 ObservableDouble、 ObservableInt等等
问题又来了,如果我使用的是第二种形式,只有一个 public String getFirstName(){}没有申明firstName这个成员域呢?(当然为了能更新值你还是要有对应的setFirstName(String)方法)
做法:
首先让你的xxViewModel 继承 BaseObservable(其实ObservableField继承了BaseObservable),然后在getFirstName()方法添加注解@Bindable,最后在setFirstName里面添加 notifyPropertyChanged(BR.firstName);BR会自动生成,firstName会称为它的成员域(static final修饰)
dataBinding双向绑定
可以让viewModel数据域的改变直接反映在控件上,那是否也可以在控件改变时直接反映在数据域上?假如有一个Edittext,它的android:text绑定了viewModel的一个数据域,我们暂时取名为input吧,那在用户对这个Edittext输入时,是否可以用Edittext中用户输入的内容改变input的值呢?改变只需一点点:
andorid:text="@={viewModel.input}"
加个“=”,当用户输入时,用户输入的内容就会直接改变input的值了。
事件处理的绑定
事件处理的绑定有两种,无论采用哪一种都会给View注册一个对应的监听器,在监听器的方法中(覆盖抽象函数的那一个)进行事件处理,如android:onClick将会给这个View注册一个点击行为的监听器,重写public onClick(View v)这个抽象函数,在这个函数中处理事件
第一种就是项目使用的那种,叫做方法引用
android:onClick="@{viewModel::search}"
类似于正常的android:onClick,然后在Activity中写public void
方法名(View v){}这种形式,只是方法应该写在对应的xxViewModel.java文件中而不是Activity,这种大家应该很快就能上手
另外一种更加方便,名为监听者绑定,其实就是Lambda表达式的使用,大家可以看看第一种,方法的参数类型一定为View,但是如果采用监听者绑定这种方式,参数可以任意,只要返回值与对应监听器的抽象函数的返回值一样就可以。拿项目中的那个例子来说,其实点击“查询”Button后,search(View v)方法做的是权限的申请,申请成功后调用 getLocate()(在这个方法里调动M层),其实这里的参数v完全没有用,能不能不写呢?或者能不能改变参数的类型和数量?
采用第一种方式是不可以的,但是如果采用第二种方法,可以
更改search的参数类型
public void search(int p) {
int sdkInt = Build.VERSION.SDK_INT;
if (sdkInt < Build.VERSION_CODES.M) {
getLocate();
return;
}
int permission = ContextCompat.checkSelfPermission
(mContext, Manifest.permission.ACCESS_FINE_LOCATION);
if (permission != PackageManager.PERMISSION_GRANTED) {//没有开启权限
ActivityCompat.requestPermissions((MainActivity) mContext,
new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
AppConfig.REQUEST_CODE);//会在MainActivity中回调
}else{
getLocate();
}
}
更改.xml
<Button
android:id="@+id/search"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="查询"
android:onClick="@{()->viewModel.search(100)}"
android:layout_marginLeft="8dp"
app:layout_constraintLeft_toLeftOf="parent"
android:layout_marginRight="8dp"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginBottom="8dp" />
ok了,实现的功能完全一样
@{()->viewModel.search(100)}中的100是我随便填的,这里使用到的是Lambda表达式(参数)->单行的函数体,可以看到search(int)返回值类型为void,跟public void
onClick(View v)一致。当然,这里省略了参数v(View类型),完整形式应该为:
android:onClick="@{(v)->viewModel.search(100)}"
这里需要注意的是如果你想要写参数,那你必须要写全,比如有些监听器的抽象函数是有多个参数的,像:
public static interface OnCheckedChangeListener {
/**
* Called when the checked state of a compound button has changed.
*
* @param buttonView The compound button view whose state has changed.
* @param isChecked The new checked state of buttonView.
*/
void onCheckedChanged(CompoundButton buttonView, boolean isChecked);
}
这时候以下两种形式都可以:
<CheckBox android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onCheckedChanged="@{(cb, isChecked) ->... }" />
<CheckBox android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onCheckedChanged="@{() ->... }" />
甚至这种形式也是OK的
android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}"
其中doSomething返回void
属性的Setters
1.自动的setter
对一个 attribute 来说,Data Binding 会尝试寻找对应的 setAttribute 函数。属性的命名空间不会对这个过程产生影响,只有属性的命名才是决定因素。举个例子:android:text="@{viewModel.firstName}",Data Binding则会寻找 setText(String),当然如果你的firstName是int类型, Data Binding则会寻找 setText(int),根据这个原则可以使用未用declare-styleable声明的自定义属性举个例子来说明:
在上面项目的activity_main.xml添加一个自定义view
<com.example.lingo.mvvmdemo.view.Card
android:layout_width="100dp"
android:background="@color/red"
android:layout_height="100dp"
android:layout_marginTop="8dp"
app:customSetter="@{viewModel.location[`latitude`]}"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
/>
这里的app:customSetter是自定义的属性,但是没有在这个项目中没有用declare-styleable申明,那怎么办,不会报错吗?添加一个 public void setCustomSetter(String str) 就不会了,DataBinding会去找这个方法。
Card.java
package com.example.lingo.mvvmdemo.view;
import android.content.Context;
import android.graphics.Color;
import android.support.annotation.Nullable;
import android.support.constraint.ConstraintLayout;
import android.util.AttributeSet;
import android.widget.TextView;
import com.example.lingo.mvvmdemo.R;
/**
* Created by lingo on 2017/7/29.
*/
public class Card extends ConstraintLayout{
private TextView textView;
public Card(Context context) {
super(context);
}
public Card(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
inflate(context,R.layout.card,this);
textView=(TextView)findViewById(R.id.card_text);
}
public void setCustomSetter(String str) {
textView.setText(str);
setBackgroundColor(Color.GREEN);
}
}
自定义的setter
看过有些文章称这个为DataBinding的最强技能,称"如果这个功能不能吸引你,那么恐怕没有什么能说服你使用 DataBinding了。"接下来就来了解一下这个被称为史上最酷的Android功能--BindingAdapter。
BindingAdapter只做一件事,就是将.xml中定义的属性值与对应的实现方法绑定在一起。
例如ImageView在XML中的android:src要求被赋予资源文件,可以我们往往从代码中动态的获取一个url,有没有办法直接在XML中通过属性设置为ImageView设置一个url,然后ImageView能显示这个url对应的图片?
其实说白了就是扩展ImageView在XML中的属性,让它更加强大和灵活
我们来看看要怎么做?
首先你可以通过attrs.xml(自己在values下建立)自定义一个属性或者就使用原有的android:src,无论是自定义属性还是原有的android:src,都要求能被赋予一个url(String类型)
step1:如果你选择扩展原有的android:src,可以跳过这一步;如果选择自定一个属性,比如命名为url,则首先要在attrs.xml添加以下代码:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="MyAttrs">
<attr name="url" format="string" />
</declare-styleable>
</resources>
step2:新建一个类,这里我命名为MyBindingAdapter.java:
public class MyBindingAdapter {
private static Context mContext;
public MyBindingAdapter(Context mContext) {
this.mContext = mContext;
}
@BindingAdapter("url")
public static void setImage1(ImageView view, String url) {
//为了简单,这里假装根据url获取了对应的Bitmap实例
Bitmap bitmap=BitmapFactory.decodeResource(mContext.getResources(), R.drawable.pic);
view.setImageBitmap(bitmap);
}
@BindingAdapter("android:src")
public static void setImage2(ImageView view, String url) {
//为了简单,这里假装根据url获取了对应的Bitmap实例
Bitmap bitmap=BitmapFactory.decodeResource(mContext.getResources(), R.drawable.pic);
view.setImageBitmap(bitmap);
}
}
其中setImage1对应自定义属性url,setImage2对应原有属性android:src,注意一个是"url",不需要命名空间,一个是"android:src“要求加上命名空间android,由于代码中使用了Context实例,可以在MainActivity.java中创建这个类的实例并传进去MainActivity.this
step3:MainActivityViewModel.java中添加一个名为url的成员域:
public ObservableField<String> url=new ObservableField<>();
public MainActivityViewModel(MainActivity mContext) {
...
url.set("http://xxxxx");//随便填的url
step4:现在就是如果使用的问题了,在activity_main.xml中添加一个ImageView:
<ImageView
android:src="@{viewModel.url}"//也可以使用自定的url app:url="@{viewModel.url}",所有自定义的属性均可用app:xxx
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginTop="8dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
/>
ImageView显示了url对应的图片,当然为了方便,我并没有真的用了url去请求对应的图片,而是用了本地的资源图片,但是思路没有错。当然尽管扩展了android:src,也不会对原有的功能有影响, android:src="@drawable/pic"仍然可用。
当然,也可以将多个属性与一个实现方法绑定在一起
例如:
@BindingAdapter(value = {"url", "placeHolder"}, requireAll = false)
public static void setImageUrl(
ImageView view, String url, int placeHolder) {
。。。
}
其中requestAll=false表示不需要同时设定两个属性也可以调用该方法
使用时:
<ImageView
app:url="@{viewModel.url}"
app:placeHolder="@{1}"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginTop="8dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
/>
当然两个属性可以不用同时设置,例如只设置 app:url="@{viewModel.url}"照样也会调用该方法,这是因为requireAll设置为false
至于事件属性,如android:onLayoutChange,可以参考谷歌安卓开发者文档,事件属性可以参考前面的事件处理的绑定或者直接在代码中设置。
其他
DataBinding最方便的三个技巧已经在上面一一介绍了,剩下的是一些比较简单的,如Custom Conversions如 android:background="@{isError ? @color/red : @color/white}",要提供一个从int转换为ColorDrawable的静态函数,这个静态函数用@BindingConversion修饰
@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
return new ColorDrawable(color);
}
另外,可以在binding表达式中添加default表达式以设置默认值,这个默认值仅在预览窗口中可以看到,运行时看不到,类似有tools
android:text="@{viewModel.location[`date`],default=`haha`}"
haha仅在预览窗口可见,运行时不可见
关于自定义生成的Binding类的名字和位置还有<include>标签和<import>标签的使用可以参考谷歌的安卓开发者文档
注意:
<data>
<import type="com.example.MyStringUtils"/>
<variable name="user" type="com.example.User"/>
</data>
…
<TextView
android:text="@{MyStringUtils.capitalize(user.lastName)}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
import导进的类型中的静态属性和静态方法可以在属性的binding(@{}表达式)表达式中使用
网友评论