Android数据绑定(DataBinding)

什么是Android数据绑定(DataBinding)?

Android数据绑定是一个Google官方发布的帮助开发者处理视图与数据交互的支持库。

数据绑定是如何工作的?

数据绑定在编译时运行,处理视图文件中发现的表达式并在应用程序中生成代码,该库包含了应用程序中的常见代码。

优点:

  • 省去了findViewById()
  • 兼容到Android2.1(API 7)
  • 不使用反射,保证了性能
  • 支持绝大部分的 Java 写法
  • 最大程度减少绑定应用程序逻辑与视图所必需的代码
  • 支持双向绑定,即数据改变时可更新视图,反之亦然
  • 支持在任意线程更新数据(RecyclerView 和 ListView的数据除外 )
  • 避免了因数据导致的空指针,当绑定的数据无效时,视图会显示绑定数据类型的默认值

Android Studio对其的支持:

  • 语法高亮显示
  • 标记错误语法
  • XML代码补全
  • 快速跳转引用

注意:数组和通用类型(如Observable类)可能会在没有错误时显示错误。

准备使用

为了更好地进行Android开发,本人强烈建议使用Android Studio并保持Android Studio 与 Gradle为最新版本

配置数据绑定使用环境:

  1. Android 数据绑定需要Android Studio 1.3及更高版本

  2. Gradle 1.5.0-alpha1及更高版本

  3. 配置相应模块(Module)的build.gradle(若其他模块要用到数据绑定也需要此配置)

    android {
        ....
        buildFeatures {
            dataBinding = true
        }
    }
    

在视图文件中绑定数据

  1. 首先准备准备一个数据类(请注意,由于视图要访问该对象的私有变量,所以必须提供getter)

    public class Person{
        private String name;
        private int age;
    
        public Person(String name, int age) {
            this.name = name;
            this.age = age;
        }
    
        public String getName() {
            return name;
        }
    
        public int getAge() {
            return age;
        }
    }
    
  2. 在相应的视图文件中引入这个数据类(根节点必须为layout)

    • 使用variable标签方式引入数据类,name为变量名,type为数据类,必须要有完整的包名(Android Studio 支持输入类名自动查找补全)
    <?xml version="1.0" encoding="utf-8"?>
    <layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
     <variable name="person" type="包名.Person" />
    </data>
    </layout>
    
    • 使用import方式引入数据类,用这种方式引入数据类还可以使用它的静态变量和方法
    <?xml version="1.0" encoding="utf-8"?>
    <layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
    <import type="包名.Person" />
    <variable name="person" type="Person" />
    </data>
    </layout>
    

    ​ 若出现不同包的同名类则可以在import 时 使用 alias 来指定一个别名,例如:

    <import type="包名.Person" alias="OtherPerson"/>
    <variable name="otherPerson" type="OtherPerson"/>
    
  3. 在视图中绑定数据,请注意数据类型匹配,可在后用defalut设置默认值,默认值会显示在预览视图中。

     <LinearLayout
           android:orientation="vertical"
           android:layout_width="match_parent"
           android:layout_height="match_parent">
           <TextView android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               android:text="@{person.name}"/>
           <TextView android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               android:text="@{String.valueOf(person.age})}"/>
       </LinearLayout>
    

    还可以绑定任何位置的点击事件,使用::来绑定点击事件

    public class ClickEvents {
        public void onBtnClick(View view) { ... }
    }
    
    <?xml version="1.0" encoding="utf-8"?>
    <layout xmlns:android="http://schemas.android.com/apk/res/android">
       <data>
           <variable name="clickEvents" type="包名.ClickEvents"/>
       </data>
       <LinearLayout
           android:orientation="vertical"
           android:layout_width="match_parent"
           android:layout_height="match_parent">
           <Button 
               android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               android:text="BTN"
               android:onClick="@{clickEvents::onBtnClick}"/>
       </LinearLayout>
    </layout>
    

    绑定Array、List、Map、Sparse的数据(不支持Set)

    String[] array = {"测试数组"};
    
    List<String> list = new ArrayList<>();
    list.add("测试集合");
    
    Map<String, String> map = new HashMap<>();
    map.put("测试1", "测试Map");
    
    SparseArray<String> sparseArray = new SparseArray<>();
    sparseArray.append(0, "测试SparseArray");
    
    binding.setArray(array);
    binding.setList(list);
    binding.setMap(map);
    binding.setSparseArray(sparseArray);
    

    注意:

    • 在xml中设置范型时要将"<>"换成相应的实体,"<" 对应 &lt;, ">" 对应 &gt;
    • 除Array外,其他的还可以用get()来取值

    <?xml version="1.0" encoding="utf-8"?>
    <layout xmlns:android="http://schemas.android.com/apk/res/android">
        <data>
            <import type="java.util.Map" />
            <import type="java.util.List" />
            <import type="java.lang.String" />
            <import type="android.util.SparseArray"/>
            <!--以下爆红正常,运行无错。-->
            <variable name="array" type="String[]" />
            <variable name="list" type="List<String>" />
            <variablename="map" type="Map<String, String>" />
            <variablename="sparseArray" type="SparseArray<String>" />
        </data>
    
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:gravity="center"
            android:orientation="vertical">
            <TextView
                android:id="@+id/tv_array"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@{array[0]}"
                android:textSize="18sp" />
            <TextView
                android:id="@+id/tv_list"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="10dp"
                android:text="@{list[0]}"
                android:textSize="18sp" />
            <TextView
                android:id="@+id/tv_map"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="10dp"
                android:text="@{map[`测试1`]}"
                android:textSize="18sp" />
            <TextView
                android:id="@+id/tv_sparseArray"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="10dp"
                android:text="@{sparseArray[0]}"
                android:textSize="18sp" />
        </LinearLayout>
    </layout>
    
    

  4. 在程序中绑定视图并设置数据

    注意:

    • 绑定数据的视图会自动根据其视图名字去掉"_"并在最后加上Binding生成驼峰式类名的绑定文件,例:activity_main => ActivityMainBinding

    • 视图中设置了id的元素会在对应的绑定类中生成一个对象,其命名方式同上,首字母小写,例:tv_name => tvName


    1. 绑定视图

      • 在Activity中

        ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        
      • 在Fragment中

        FragmentMainBinding binding = DataBindingUtil.inflate(inflater, R.layout.fragment_main, container, false);
        // or
        FragmentMainBinding binding = FragmentMainBinding.inflate(inflater, container, false);
        
      • 在RecyclerView或者ListView中

        ItemBinding binding = ItemBinding.inflate(layoutInflater, viewGroup, false);
        //or
        ItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);
        
    2. 设置数据

      Person person = new Person("GavinRowe", 21);
      binding.setPerson(person);
      

    运行程序就能看到数据了!

改变数据并通知视图

  • 继承BaseObservable方式,使用数据类继承BaseObservable

    public class Person extends BaseObservable{
        private String name;
        private int age;
      
        public Person(String name, int age) {
            this.name = name;
            this.age = age;
        }
      
      @Bindable
        public String getName() {
            return name;
        }
      
      @Bindable
        public int getAge() {
            return age;
        }
      
          public void setName(String name) {
            this.name = name;
            notifyPropertyChanged(BR.name);
        }
      
          public void setAge(int age) {
            this.age = age;
          notifyPropertyChanged(BR.age);
        }
      
    }
    

    标注变量写法

    private @Bindable String name;
    

    继承BaseObservable后将会获得两个公共的通知视图更新的方法:1. 通知所有数据更新 notifyChange(); 2. 通知特定数据更新 notifyPropertyChanged(int fieldId),参数为BR.java文件中对应的变量标志。

    被@Bindable注解标注的getter或者变量将会在一个位于包名下的BR.java文件中生成一个对应的变量标志,例:上面已经被标注的getName()与getAge()对应BR.name与BR.age。

    注:@Bindable注解不是必须的,不使用时就必须调用notfyChange()或notifyPropertyChanged(BR._all)来通知视图更新所有数据

  • 使用ObservableField方式

    public class Person{
        public ObservableField<String> name = new ObservableField<>() ;
        public ObservableInt age = new ObservableInt();
      
        public Person(String name, int age) {
            this.name.set(name);
            this.age.set(age);
        }
      
    }
    

    对应的ObservableField将会提供getter和setter,通过setter设置数据后会自动通知视图更新数据

双向绑定

双向绑定,即数据改变时可通知视图改变,视图改变时同时改变数据

用法:在绑定数据时将@{data} 变为 @={data}

  1. 数据类

    public class Person{
        public ObservableField<String> name = new ObservableField<>();
        public Person(String name) {
            this.name.set(name);
        }
    }
    
  2. 视图绑定数据

    <?xml version="1.0" encoding="utf-8"?>
    <layout xmlns:android="http://schemas.android.com/apk/res/android">
    <import type="包名.Person" />
    <variable name="person" type="Person" />
      <LinearLayout
           android:orientation="vertical"
           android:layout_width="match_parent"
           android:layout_height="match_parent">
           <TextView
               android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               android:text="@{person.name}"/>
         <EditText
                android:gravity="center"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:inputType="text"
                 android:text="@={person.name}"/>
       </LinearLayout>
    </layout>
    
  3. 绑定视图并设置数据

     ActivityMainBinding binding = DataBindingUtil.setContentView(this,R.layout.activity_main);
     binding.setPerson(new Person("GavinRowe"));
    
双向绑定

绑定数据在RecyclerView中的应用

通过一个多布局RecyclerView来演示数据绑定

  1. 数据类

    public class Person {
        public ObservableField<String> name = new ObservableField<>();
        public ObservableInt age = new ObservableInt();
        public Person(String name, int age) {
            this.name.set(name);
            this.age.set(age);
        }
    }
    
  2. 页面视图,由于要为RecyclerView设置适配器以及LayoutManager所以需要为它设置一个ID便于查找

    <?xml version="1.0" encoding="utf-8"?>
    <layout xmlns:android="http://schemas.android.com/apk/res/android">
        <data>
            <variable name="person" type="包名.Person" />
        </data>
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:gravity="center"
            android:orientation="vertical">
            <Button
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:onClick="onAddDataClick"
                android:text="添加数据" />
            <android.support.v7.widget.RecyclerView
                android:id="@+id/rv_people"
                android:layout_width="match_parent"
                android:layout_height="match_parent" />
        </LinearLayout>
    </layout>
    
  3. item_rv_people_01视图

    <?xml version="1.0" encoding="utf-8"?>
    <layout>
        <data>
            <variable name="person" type="包名.Person" />
        </data>
        <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
            android:id="@+id/item_rv_people"
            android:layout_width="match_parent"
            android:layout_height="40dp"
            android:gravity="center"
            android:paddingEnd="12dp"
            android:paddingStart="12dp">
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@{person.name}"
                android:textSize="16sp" />
            <TextView
                android:id="@+id/tv_age"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginStart="10dp"
                android:text="@{String.valueOf(person.age)}"
                android:textSize="16sp" />
        </LinearLayout>
    </layout>
    

  4. item_rv_people_02视图

    <?xml version="1.0" encoding="utf-8"?>
    <layout>
        <data>
            <variable name="person" type="包名.Person" />
        </data>
        <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
            android:id="@+id/item_rv_people"
            android:layout_width="match_parent"
            android:layout_height="40dp"
            android:gravity="center"
            android:paddingEnd="12dp"
            android:paddingStart="12dp">
            <TextView
                android:id="@+id/tv_age"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@{String.valueOf(person.age)}"
                android:textSize="16sp" />
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginStart="10dp"
                android:text="@{person.name}"
                android:textSize="16sp" />
        </LinearLayout>
    </layout>
    
  5. MultiLayoutPeopleAdapter多布局适配器

    注意:

    • ViewHolder的写法,通过ViewDataBinding父类来接收两个不同视图的绑定类,两个布局共享的数据就是person,由于ViewDataBinding没有setPerson(),所以通过setVariable(BR.person, person) 方法设置键值对的方式来将Person对象绑定到两个不同的视图
    • getItemViewType(int position)直接返回对应Item视图的ID,通过DataBindingUtil就可以绑定任何想绑定的视图了
    • 关于executePendingBindings(),当你的数据改变时,数据绑定在一个动画帧之前刷新,executePendingBindings()可以立即强制刷新,此操作必须在UI线程进行
    • 若要分别对视图操作,则可将绑定类引用向下转型,然后分别获取视图来设置进行操作
    public class MultiLayoutPeopleAdapter extends RecyclerView.Adapter<MultiLayoutPeopleAdapter.PeopleViewHolder> {
        private List<Person> people;
        private static Activity mActivity;
        MultiLayoutPeopleAdapter(Activity activity, List<Person> people) {
            mActivity = activity;
            this.people = people;
        }
        @Override
        public PeopleViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            return PeopleViewHolder.create(LayoutInflater.from(parent.getContext()), parent, viewType);
        }
    
        @Override
        public void onBindViewHolder(PeopleViewHolder holder, final int position) {
            holder.bindTo(people.get(position));
            // 判断布局
            if (holder.mBinding instanceof ItemRvPeople01Binding) {
                ItemRvPeople01Binding item01 = (ItemRvPeople01Binding) holder.mBinding;
                item01.itemRvPeople.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        Toast.makeText(mActivity, "item01的" + position + "被点了!", Toast.LENGTH_SHORT).show();
                    }
                });
            } else {
                ItemRvPeople02Binding item02 = (ItemRvPeople02Binding) holder.mBinding;
                item02.itemRvPeople.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        Toast.makeText(mActivity, "item02的" + position + "被点了!", Toast.LENGTH_SHORT).show();
                    }
                });
            }
        }
    
        @Override
        public int getItemCount() {
            return people.size();
        }
    
        @Override
        public int getItemViewType(int position) {
            if (position % 2 == 0) {
                return R.layout.item_rv_people_01;
            } else {
                return R.layout.item_rv_people_02;
            }
        }
    
        static class PeopleViewHolder extends RecyclerView.ViewHolder {
    
            ViewDataBinding mBinding;
    
            static PeopleViewHolder create(LayoutInflater inflater, ViewGroup parent, int type) {
                ViewDataBinding binding = DataBindingUtil.inflate(inflater, type, parent, false);
                return new PeopleViewHolder(binding);
            }
    
            private PeopleViewHolder(ViewDataBinding binding) {
                super(binding.getRoot());
                mBinding = binding;
            }
    
            void bindTo(Person person) {
                mBinding.setVariable(BR.person, person);
                mBinding.executePendingBindings();
            }
        }
    }
    
  6. 绑定视图

     private List<Person> people;
     private MultiLayoutPeopleAdapter multiLayoutPeopleAdapter;
     @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
            people = new ArrayList<>();
            multiLayoutPeopleAdapter = new MultiLayoutPeopleAdapter(this, people);
            binding.rvPeople.setLayoutManager(new LinearLayoutManager(this));
            binding.rvPeople.setAdapter(multiLayoutPeopleAdapter);
        }
    
        public void onAddDataClick(View view) {
            people.add(new Person("国哥", 21));
            people.add(new Person("哥哥", 27));
            people.add(new Person("姐姐", 30));
            people.add(new Person("小红", 16));
            people.add(new Person("小蓝", 15));
            people.add(new Person("小橙", 14));
            people.add(new Person("小绿", 13));
            people.add(new Person("小黄", 12));
            people.add(new Person("小花", 6));
            people.add(new Person("小德", 5));
            people.add(new Person("小梦", 4));
            multiLayoutPeopleAdapter.notifyDataSetChanged();
        }
    
多布局RecyclerView

注解:@Bindable

此注解可用来标注变量和getter,被标注后将会在一个位于包名下的BR.java文件中生成一个对应的变量标志。

注解:@BindingAdapter

此注解可用来标注方法,当xml中使用到该属性时就会调用其标注的方法。

单参用法:@BindingAdapter("xml属性"),参数可以为已有的xml属性,比如android:src,也可以自定义属性直接在xml中使用,若自定义属性不带命名空间(如:android:, app:, xxx:等 )将默认为app:,在使用的时候请注意声明命名空间,如:xmlns:app="http://schemas.android.com/apk/res-auto"

多参用法:@BindingAdapter(value = {"imgUrl", "android:clickable"}, requireAll = false),requireAll表示是否为每个声明的属性添加绑定值,默认为true。

注意:

  • 标注的方法第一个参数必须为对应的视图对象
  • 标注的方法参数顺序必须与标注的xml属性顺序一致
  • 在xml使用标注的属性时,其值必须用数据绑定的形式
  • 标注的方法为实例方法时,该类必须先实现DataBindingComponent,然后在相应绑定类解析视图之前调用DataBindingUtil.setDefaultComponent

例:

public class Concat {
    public static String content = "测试Binding";
    @BindingAdapter("android:text")
    public static void add(final TextView tv, String content) {
        Log.d("Concat", content);
        tv.setText(content.concat("Adapter"));
    }
}
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable name="concat" type="包名.Concat" />
    </data>
   <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{concat.content}"
            android:textSize="18sp" />
</layout>
@BindingAdapter注解

XML中绑定数据支持的表达式

  • 数学 + - / * %
  • 字符串连接 +
  • 逻辑 && ||
  • 二进制 & | ^
  • 一元运算 + - ! ~
  • 三元运算 ?:
  • 判断是否为空 ??(例:android:text="@{user.name ?? user.defaultName}",相当于android:text="@{user.name !=null ? user.name : user.defaultName}")
  • 位运算 >> >>> <<
  • 比较 == > < >= <=
  • instanceof
  • 方法调用
  • 变量引用
  • 获取数组、集合、Map的值 []

不支持:this, super, new

建议在视图中用与视图相关的简单明了的表达式,否则建议使用方法或者@BindingAdapter

错误信息

在这里我会提供一些使用数据绑定时曾遇到过的错误和可能的原因,仅供参考!

更多的如果我遇到了会更新出来,如果正在阅读此文的你遇到过一些我没有提到的问题欢迎联系我

  • android.content.res.Resources$NotFoundException 数据类型错误导致,例:给android:text绑定数据时,该数据类型为int

  • Error:(6, 27) 错误: 找不到符号 符号: 类 DataBindingComponent 位置: 程序包 android.databinding 遇到此类错误请往下拉,一般在后面会有具体错误原因

    Error:(55, 29) Could not find accessor… 绑定数据时xml参数名写错了或者访问的私有变量未提供getter

    Error:(148, 30) Identifiers must have user defined types from the XML file 某个数据未在xml的data标签进行导入,如果已在data标签中用variable标签导入,检查绑定位置是否用类名来引用其数据,若是,换成import标签导入数据​

觉得还不够?

传送门:
官方DataBinding API
官方DataBinding 使用手册

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343