欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Android进阶六:Databinding的双向绑定

程序员文章站 2022-04-20 08:31:36
...

在Android Studio 2.1 Preview 3之后,官方开始支持双向绑定了。
什么是双向绑定呢?
下面是Data Binding的基本使用:

<layout ...>
  <data>
    <variable type="com.example.myapp.User" name="user"/>
  </data>
  <RelativeLayout ...>
    <EditText android:text="@{user.name}" .../>
  </RelativeLayout>
</layout>

上面的EditText的text属性和对象user的name属性进行了绑定,但是代码如果只像上面这样这样的话,那EditView的text只会在第一次设置binding.setUser(user)的时候被赋予user.name值,之后如果user.name值变化的时候,EditView的text值不会变化。

我们理想的情况是user.name值变化的时候,EditText的text值也同时自动变化,同样,EditText的text值变化的时候,user.name值也同时自动变化。

那所谓双向绑定,包括正向绑定和反向绑定,

正向绑定:当user的name值变化的时候,EidtView的text也同时变化;

反向绑定:当EidtView的text变化的时候,user对象的name值也同时变化。

怎么实现正向绑定呢?

正向绑定

方法一:继承Observable的方式

具体如下:

1.我们的实体类(User类)继承BaseObservale类

2.Getter上使用注解@Bindable

3.在Setter里调用方法notifyPropertyChanged

让User类继承BaseObservable:

public  class User extends BaseObservable
{
    private String name;
    @Bindable    
    public String getName() {        
        return username;  
    }  
    public void setName(String name) {        
        this.name = name;        
        notifyPropertyChanged( BR.name);    
    }    
}    

注意:当Getter方法getName()上加@Bindable以后,会自动在com.android.databinding.library.baseAdapters包的BR类中加了name属性

原理:当代码中设置user的name属性的时候user.setName(name),调用notifyPropertyChanged(BR.name),这个方法在BaseObservable类中定义,它会根据BR.name这个ID去通知相应的UI进行更新。以上就在user的name属性变化以后,EditView的text会自动的发送变化,就实现了正向绑定。

方法二:定义ObservableField方式

如果所有要绑定的都需要创建Observable类,那也太麻烦了。所以Data Binding还提供了一系列Observable,包括 ObservableBoolean, ObservableByte, ObservableChar, ObservableShort, ObservableInt, ObservableLong, ObservableFloat, ObservableDouble, 和ObservableParcelable。我们还能通过ObservableField泛型来申明其他类型,如:

private static class User {
   public final ObservableField<String> firstName =new ObservableField<>();
   public final ObservableField<String> lastName =new ObservableField<>();
   public final ObservableInt age = new ObservableInt();
}

而在xml中,使用方法和普通的String,int一样,只是会自动刷新,但在java中访问则会相对麻烦:

user.firstName.set("Google");
int age = user.age.get();

那怎么实现反向绑定呢?
对于一部分属性,在XML中的引用中把“@{}”改成“@={}”,比如:

<layout ...>
  <data>
    <variable type="com.example.myapp.User" name="user"/>
  </data>
  <RelativeLayout ...>
    <EditText android:text="@={user.name}" .../>
  </RelativeLayout>
</layout>

这样当EditText的text发生改变的时候user.name属性也会同步变化,只有少部分控件的属性都支持“@={}”,如下:

  • AbsListView android:selectedItemPosition

  • CalendarView android:date

  • CompoundButton android:checked

  • DatePicker android:year, android:month, android:day

  • NumberPicker android:value

  • RadioGroup android:checkedButton

  • RatingBar android:rating

  • SeekBar android:progress

  • TabHost android:currentTab (估计没人用)

  • TextView android:text

  • TimePicker android:hour, android:minute

反向绑定

现在在来看下自定义属性如何实现反向绑定,
自定义一个SeedBar控件,可以调整最大最小值,调整HMIN,MHAX更新RangeBean的min和max属性:

Android进阶六:Databinding的双向绑定

Data Bean类如下:

public  class RangeBean
{
    int max;
    int min;

    public int getMax()
    {
        return max;
    }

    public void setMax(int max)
    {
        this.max = max;
    }

    public int getMin()
    {
        return min;
    }

    public void setMin(int min)
    {
        this.min = min;
    }   
}

自定义控件RangeSeekbar如下:

public class RangeSeekbar extends View {
    ……
    private int max,min;
    public void setMax(int max)
    {
        this.max = max;
        invalidate();
    }
    public void setMin(int min)
    {
        this.min = min;
        invalidate();
    }


    public int getMax()
    {
        return this.max;
    }

    public int getMin()
    {
        return this.min;
    }

    ……
}

布局中添加RangeSeekbar:

<com.biyou.view.RangeSeekbar
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/id_rangeseekbar"
        app:min = "@{rangeBean.min}"
        app:max = "@{rangeBean.max}"
        app:thumbdrawable = "@drawable/seekbar_thumb"
        app:backgrounddrawable="@drawable/seekbar_bg_normal"
        app:foregrounddrawable="@drawable/seekbar_fg_normal"/>

添加反向绑定:

1.添加@InverseBindingMethods

接下来我们要修改RangeSeekbar类,在类的顶部添加:

@InverseBindingMethods({
        @InverseBindingMethod(
                type = com.biyou.RangeSeekbar.class,
                attribute = "min",
                event = "minAttrChanged",
                method = "getMin"),
       @InverseBindingMethod(
                type = com.biyou.RangeSeekbar.class,
                attribute = "max",
                event = "maxAttrChanged",
                method = "getMax")})
public class RangeSeekbar extends View {

}

其中event和method不是必须的,因为系统会自动根据attribute值生成,

  • type 是需要反向绑定的类,一般是View或者ViewGroup,

  • attribute 是需要反向绑定的属性,本例是min和max,

  • eventmethod 的结构分别是:属性+AttrChanged,get+属性,一般不需要设置,系统自动生成。

method的定义还可以直接在方法上,比如min属性对应的getter:

   @InverseBindingAdapter(attribute = "min", event = "minAttrChanged")
    public int getMin()
    {
        return this.min;
    }

2.添加@BindingAdapter,设置监听

    private static InverseBindingListener minInverseBindingListener;
    private static InverseBindingListener maxInverseBindingListener;
   @BindingAdapter("minAttrChanged")
    public static void setMinChangedListener(RangeSeekbar rangeSeekbar,final InverseBindingListener bindingListener) {
        minInverseBindingListener = bindingListener;
    }

    @BindingAdapter("maxAttrChanged")
    public static void setMaxChangedListener(RangeSeekbar rangeSeekbar,final InverseBindingListener bindingListener) {
        maxInverseBindingListener = bindingListener;
    }

注意方法名为:set+属性+ChangedListener,这个方法会在生成的databinding类中被调用

3.触发监听

当HMIN和HMAX进度条被拖动时,调用minInverseBindingListener和maxInverseBindingListener的onChange()方法,
onChange()方法会去改变rangeBean对象的min和max属性值:

public boolean onTouchEvent(MotionEvent event) {
        int action = event.getActionMasked();
        switch (action)
        {
            case MotionEvent.ACTION_DOWN:
                LogManager.LogEWithTag("GOODLUCK","ACTION_DOWN");
                mStartX = (int) event.getX();
                mStartY = (int) event.getY();
                if(mLeftCursorRect.contains(mStartX,mStartY))
                {
                    if(mLeftHited)
                        break;
                    mLeftHited = true;
                }
                else if(mRightCursorRect.contains(mStartX,mStartY))
                {
                    if(mRightHited)
                        break;
                    mRightHited = true;
                }
                break;
            case MotionEvent.ACTION_MOVE:
                int moveX = (int) event.getX();
                int moveY = (int) event.getY();
                if(mLeftHited)
                {
                    if(mLeftCursorRect.left + moveX - mStartX < 0 ||
                            mLeftCursorRect.left + moveX - mStartX > (mSeekbarRect.right - mSeekbarRect.left))
                        return true;
                    mLeftCursorRect.left = mLeftCursorRect.left + moveX - mStartX;
                    mLeftCursorRect.right = mLeftCursorRect.left + mLeftCursor.getIntrinsicWidth();
                    this.min = (int) ((((float)mLeftCursorRect.left)/(mSeekbarRect.right - mSeekbarRect.left)) *255);
                    this.max = (int) ((((float)mRightCursorRect.left)/(mSeekbarRect.right - mSeekbarRect.left)) *255);
                    if(this.onSeekBarChangeListener != null) {
         this.onSeekBarChangeListener.OnSeekBarChange(min,max);
                    }
                    else
                    {
                        minInverseBindingListener.onChange();
                    }

                }
                else if(mRightHited)
                {
                    if(mRightCursorRect.left + moveX - mStartX < 0 ||
                            mRightCursorRect.left + moveX - mStartX > (mSeekbarRect.right - mSeekbarRect.left))
                        return true;
                    mRightCursorRect.left = mRightCursorRect.left + moveX - mStartX;
                    mRightCursorRect.right = mRightCursorRect.left + mRightCursor.getIntrinsicWidth();
                    this.min = (int) (((float)mLeftCursorRect.left)/(mSeekbarRect.right - mSeekbarRect.left) *255);
                    this.max = (int) (((float)mRightCursorRect.left)/(mSeekbarRect.right - mSeekbarRect.left) *255);
                    if(this.onSeekBarChangeListener != null) {

                        this.onSeekBarChangeListener.OnSeekBarChange(min,max);
                    }
                    else
                    {
                        maxInverseBindingListener.onChange();
                    }
                }
                if(mLeftCursorRect.left < mRightCursorRect.left) {
                    mSeekbarFgRect.left = mLeftCursorRect.left + mLeftCursor.getIntrinsicWidth() / 2;
                    mSeekbarFgRect.right = mRightCursorRect.left + mRightCursor.getIntrinsicWidth() / 2;
                }
                else
                {
                    mSeekbarFgRect.left = mRightCursorRect.left + mLeftCursor.getIntrinsicWidth() / 2;
                    mSeekbarFgRect.right = mLeftCursorRect.left + mRightCursor.getIntrinsicWidth() / 2;
                }
                mStartX = moveX;
                mStartY = moveY;
                invalidate();
                break;
            case MotionEvent.ACTION_UP:
                mLeftHited = false;
                mRightHited = false;
                break;
        }
        return true;
    }

别忘了布局中把min和max的属性值,加上”@={}“:

    <com.tomra.trsort.ui.view.RangeSeekbar
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/id_rangeseekbar"
        app:min = "@={classBean.hMin}"
        app:max = "@={classBean.hMax}"
        app:thumbdrawable = "@drawable/seekbar_thumb"
        app:backgrounddrawable="@drawable/seekbar_bg_normal"
        app:foregrounddrawable="@drawable/seekbar_fg_normal"/>

避免死循环

双向绑定有可能会出现死循环,因为当你通过Listener反向设置数据时,数据也会再次发送事件给View。所以我们需要在设置一下避免死循环:

 public void setMax(int max)
    {
        if(this.max == max)
            return;
        this.max = max;
        invalidate();
    }
    public void setMin(int min)
    {
        if(this.min == min)
            return;
        this.min = min;
        invalidate();
    }

当值相等的时候返回,不更新UI。