Android进阶六:Databinding的双向绑定

在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属性:

图111

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。

发布了83 篇原创文章 · 获赞 55 · 访问量 23万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览