Android Data Binding

Android Data Binding  https://developer.android.google.cn/topic/libraries/data-binding/index.html 

最近做完了新项目,里面用到了,  lifecycle,  Android Data Binding, mvvm
基础的只是就不复制黏贴了,网上太多了, 下面只总结经验

其中有几篇讲的很细的,还有其他的也可以留着查
https://blog.csdn.net/jjwwmlp456/article/details/54915981  Android Data Binding Library 官方文档(译)
https://blog.csdn.net/yzy1226466341/article/category/6703506  Data Binding 
https://blog.csdn.net/qiang_xi/article/details/75379321     Data Binding 1 2 3 4 
https://blog.csdn.net/qq_27630169/article/details/52595913   转换

https://blog.csdn.net/qq_33689414/article/details/52205718



一、Data Binding Library (数据绑定库),旨在减少绑定应用程序逻辑和布局所需的一些耦合性代码 

    最低支持Android 2.1 (API Level 7)

构建环境

Data binding只支持Android Studio的开发
Android Studio的版本必须在1.3版本及以上,BuildToolsVersion必须在22.0.1以上。
以上条件达到以后,只需再项目的build.gradle文件中的android节点下增加以下代码使项目支持Databinding,这样的话就可以使用这个框架了。

使用gradle插件1.5-alpha1以上 
在build.gradle添加如下声明

android {
     ....
    dataBinding {
        enabled = true
    }
}
为什么配置了 dataBinding{enabled = true}之后就可以使用dataBinding方式进行开发了?
   在Android Studio中是依靠gradle来管理项目的,在创建一个项目时,从开始创建一直到创建完毕,整个过程是需要执行很多个gradle task的,
   这些task有很多是系统预先帮我们定义好的,比如build task,clean task等,DataBinding相关的task也是系统预先帮我们定义好的,
   但是默认情况下,DataBinding相关的task在task列表中是没有的,因为我们没有开启dataBinding,但是一旦我们通过 dataBinding{enabled = true}的方式开启DataBinding之后,
   DataBinding相关的task就会出现在task列表中,每当我们执行编译之类的操作时,就会执行这些dataBinding Task, 这些task的作用就是检查并生成相关dataBinding代码,
   比如dataBindingExportBuildInfoDebug这个task就是用来导出debug模式下的build信息的。
   项目中所有的task列表可以从Android Studio面板右侧贴边的Gradle中找到。


二、工作原理 

 DataBinding开发实际上与原始的Android开发没有过多的区别,只是极大的简化了代码对控件的操作,
允许程序员直接在布局上进行数据的赋值,我们只需要在代码中对布局中的文件变量进行赋值操作,操作之后界面便会实现相应的改变。
我们需要做的改变只是将布局文件的根布局改为,layout节点下有data和真正的layout节点(也就是LinearLayout等布局方式),

Android Studio为什么有能力生成DataBinding相关代码


我们知道Android Studio可以自动生成DataBinding相关代码。

在以前,如果我们想使用dataBinding方式开发,还需要引入dataBinding的相关Library,但是现在我们不用引入这些library了,因为在我们配置完dataBinding{enabled = true}之后,系统会自动引入这些library,但是这些library的源码在哪里放着呢?我们可以找到Android Studio的安装路径,在 
\plugins\android\lib路径下找到data-binding.jar,比如我的路径则为:D:\Android\AndroidStudio\plugins\android\lib,这个jar包中存放的就是DataBinding相关的源码,包括annotationprocessor,parser,tool,以及相关注解和类,这个源码就是让Android Studio有能力生成DataBinding相关代码的原因。
但是我们发现这些源码都已经被编译为class文件,想查看真正的源码还想看到对应的注释通过这个源码是不可能了。我们可以通过这个地址DataBinding的java源码地址 查询和下载DataBinding各个版本的源码和Javadoc。


三.使用方法

DataBinding总共有如下几个标签可以使用,一些标签下还有一些属性可以更具体的控制这些标签: 
- layout标签 
- data标签:class属性 

- variable标签:type属性、name属性 

- import标签:type属性、alias属性

这些标签以及属性的定义和使用都可以在compilerCommon

这个jar包中的android/databinding/tool/store/LayoutFileParser.java这个类中找到。


(1) 自定义的监听事件

//根据实际需求,填写方法需要的参数
public class ViewActionListener {

    //无返回值
    public void testClick(){}

    public void testClick(View view){}

    public void testClick(View view ,name, age ){} // view 这个参数可以带也可以不带,不过一般都是需要带上的

    //有返回值
    public boolean buttonClick(View view ){}

}

(2) @{} 为单向的绑定  , @ ={} 为双向的绑定

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
   <data>

       <import type ="android.view.View">                        //导入类

       <import type ="com.example.my.View" alias="renameView"/>  //类名都是View(上面也是View) 可以重命名

       <import type ="com.example.MyUtils"/>  //导入工具类

       <variable name="testInt" type="int"/>  //定义常量

       <variable name="testList" type="List<User>"/>  //引入list

       <variable name="testMap" type="Map<String, String>"/> //引入map
       
       <variable name="testBean" type="com.example.TestBean"/>  //引入实体类

       <variable name="testModel" type="com.example.MainModel"/>  //引入model
       
       <variable name="testAdapter" type="com.example.MainAdapter"/>  //引入adapter

       <variable name="testActivity" type="com.example.MainActivity"/>  //引入Activity

       <variable name="testFragment" type="com.example.MainFragment"/>   //引入fragment

       <variable name="listener" type="com.example.ViewActionListener"/>   //引入listener

   </data>

   
   //对 include的布局传递数据
   <include layout="@layout/tow_layout"
                app:data="@{testList}"/>


       <TextView 
           android:id="@+id/m_test1"
	   /***  赋值   ***/
	   android:text="@{testClass.data}"   //设置实体类的值

	   android:text="@{testList[index]}"  //设置list的值 testList[testActivity.num]

	   android:margin ="@{testBean.padding ? @dimen/padding : @dimen/no}" 

	   android:text="@{` lalal `+ MyUtils.rename()}" //字符串拼接

	   android:text='@{String.valueOf( 10 + (Integer)testList["age"] + (Integer) testBean.age )}'

	   android:text="@{MyUtils.rename()}" //静态的属性方法直接使用

           //合并null操作符,使用 “??” 操作符
	   android:text="@{testBean.Name ?? testBean.age}" //等于  android:text="@{testBean.Name == null ?? testBean.age: testBean.Name}"

           android:text="@{ @string /Stringneddvalue(firstName, lastName)}"   //给string动态赋值显示
	   

           android:color="@{ @string /Stringneddvalue(firstName, lastName)}"   //设置color


	   android:visibile="@{ if (testActivity.show ? View.Visibile : View.GONE )}"  //view 是否显示
	   
           
	   android:text="@{user.firstName, default=PLACEHOLDER}"  //设置default值


	   android:text="@{StringUtils.show(context,entry.text)}"  //引用 context


           /*** 设置listener  ***/
	   android:onClick="@{listener::testClick}"

	   android:onClick="@{(view) -> listener.testClick(view,testInt,testActivity.name)}"
	   
	   // 比如 null for reference types, 0 for int, false for boolea, void 
	   android:onClick="@{(view) -> view.isVisible() ? doSomething() : void}"




          /*** 双向绑定,更改实体类中的数据 
	  a nd roid :text=”@ ={userInfo.name}”就是实现的 一种双向的绑定, Activity值改变会刷新数据,EditText输入文本以后会改变Activity对应的实体,
	  如此形成绑定以后则无需再关心layout中的标签了。 布局中的id与na me的绑定就是数据的单向绑定与双向绑 定 , @{ } 为单向的绑定 ,@ ={}为双向的绑定。
	  ***/ 
          android:text="@={userInfo.name}"
          
	   /> 


        //可以在表达式中直接引用带 id 的 view,引用时采用驼峰命名法
	
	<TextView
	    android:text="@{user.entry}"
	    android:layout_width="wrap_content"
	    android:layout_height="wrap_content"
	    android:visibility="@{mTest1.getVisibility() == View.GONE ? View.GONE : View.VISIBLE}" />

	    //这里TextView直接引用第一个TextView的id

      
</layout>

java 代码中的一些操作

 MainActivityBinding binding = DataBindingUtil.setContentView(this, R.layout.main_activity);

 //model 生成 并关联
 binding.setTestModel(mainModel)

 //生成数据并绑定
 binding.setTestBean(new TestBean)

 //根据id 塞入数据
 binding.setVariable(BR.testInt, 2);

 //如果普通数据,刷新imageUrl的value
 binding.notifyPropertyChanged(BR.imageUrl);


 //activity 或 fragment并关联
 binding.setTestActivity(this);
 binding.setTestFragment(this);

 // listener 关联
 binding.setListener(this);

 //获取 contentView
 binding.getRoot();
 
 //获取控件对象,直接操作
 binding.textView.setText();

 binding.testClass = xxxx;


Binding,ActivityMainBinding这个类从哪来的

在MainActivity我们先不看别的,我们看见了一个ActivityMainBinding的类,看这个类名就大概能猜到这个类是与data binding框架相关的,

但是究竟是何种关系呢?实际上呢,ActivityMainBinding就是根据layout文件编译生成的,也就相当于一个layout转化的一个Java类,

(1)但是如果你在实际编写代码的过程中,你会发现并没有执行编译、运行之类等操作,ActivityMainBinding这个类就直接能用了,
竟然还有这种操作?其实是Android Studio 这个IDE自动帮我们做了这一步,在默认情况下,系统会使用Android Studio为我们自动生成databinding相关的代码,
但是这种方式生成的代码不能调试,如果你想通过点击ActivityMainBinding跳转到它的源码中,你会发现并不能如你所愿,而是会跳转到对应的布局文件中。
那么如果我们确实要查看ActivityMainBinding的源码并且还想调试,我们就需要通过另外一种方式:手动编译代码。这两种方式可以通过Android Studio的设置面板修改。 
 settings -> editor -> data binding 有相关设置

(2)下面我们来看看它的转换规则。 

首先我们看看它的命名规则,这个我们新建以后自动生成的layout文件叫做activity_main.xml,而我们自动生成的Binding类叫ActivityMainBinding,
生成的规则是以下划线作为分割线,首字母变成大写,以Binding结尾,Binding类的存放是在app\build\generated\source\apt\debug\com\wshuttle\trailerplatform\databinding的路径下的。 
我们再看看Binding类内部的生成规则。Binding类会对在layout中data节点下定义的变量都生成一个私有的变量值,并为它生成get和set的方法,
方便我们直接对变量进行赋值;会对layout中有id的控件生成一个public的变量,可如同代码中bing.listView获取layout的listview,并且控件的类型获取时已经确定了, 

不再需要强制转换等任何的操作。

View中使用id

在布局中的每个使用了id的view,都会在binding class中创建出一个同名的public属性。这种绑定机制,比findViewById更快速。例:

 <TextView 
  android:id="@+id/lastName
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:text="@{user.lastName}"
  />

在binding class中将会生成:

public final TextView lastName;



Variable

将为每个variable生成getter和setter方法。如

<data>
    <import type="android.graphics.drawable.Drawable"/>
    <variable name="user"  type="com.example.User"/>
    <variable name="image" type="Drawable"/>
    <variable name="note"  type="String"/>
</data>

上面的代码将生成:

public abstract com.example.User getUser();
public abstract void setUser(com.example.User user);
public abstract Drawable getImage();
public abstract void setImage(Drawable image);
public abstract String getNote();
public abstract void setNote(String note);




自定义绑定类名(一般用不到)

自定义绑定类名

默认情况下,binding 类的名称取决于布局文件的命名,以大写字母开头,移除下划线,后续字母大写并追加 “Binding” 结尾。
这个类会被放置在 databinding 包中。举个例子,布局文件 activity_main.xml 会生成 ActivityMainBinding 类。
如果 module 包名为 com.example.my.app ,binding 类会被放在 com.example.my.app.databinding 中。

通过修改 data 标签中的 class 属性,可以修改 Binding 类的命名与位置。举个例子:

<data class="CustomBinding">
    ...
</data>

以上会在 databinding 包中生成名为 CustomBinding 的 binding 类。如果需要放置在不同的包下,可以在前面加 “.”:

<data class=".CustomBinding">
    ...
</data>

这样的话, CustomBinding 会直接生成在 module 包下。如果提供完整的包名,binding 类可以放置在任何包名中:

<data class="com.example.CustomBinding">
    ...
</data>


使用注意事项

表达式语言

表达式语言与 Java 表达式有很多相似之处。下面是相同之处:

数学计算 + - / * %
字符串连接 +
逻辑 && ||
二进制 & | ^
一元 + - ! ~
位移 >> >>> <<
比较 == > < >= <=
instanceof
组 ()
字面量 - 字符,字符串,数字, null
类型转换
函数调用
字段存取
数组存取 []
三元运算符 ?:


描述	转义字符	十进制	显示结果
空格	 ;	 ;	 
小于号	<;	<;	<
大于号	>;	>;	>
与号	&;	&;	&
引号	";	";	"
撇号	&apos;	';	'
乘号	×;	×;	×
除号	÷;	÷;	÷


不支持的操作符
一些 Java 中的操作符在表达式语法中不能使用。

this
super
new
显示泛型调用<T>

四、 交互 activity与layout的交互(PS: 如果不定义var 变量,但是就必须每次改变值都调用bing.set的方法)

public ObservableBoolean show = new ObservableBoolean(); 
public ObservableInt imageUrl = new ObservableInt(R.mipmap.ic_launcher); 
private OrderInfo orderInfo;
..
...

观察对象,观察字段和观察集合。(上面实际用到了数据的赋值, 但是当数据变化时,如何自动赋值过去)

(1)Observable Objects
 A class implementing the Observable interface ,将被允许附加一个listener,来监听对象所有属性的改变

Observable interface 有一个机制来添加和删除listeners,但需要通知开发者。为了让开发变得更容易,

提供一个基类,BaseObservable,它实现创建listener的注册机制。当属性数据改变时,数据类实现者需要响应通知。

这是通过 在get ter上使用 一个注解 @Bindable,并在setter中进行通知。


private  class TestBean extends BaseObservable {
   
   private String name;

   public Test(){
   }

   public Test(String name){
    this.name = name;
   }

   @Bindable
   public String getName() {
       return this.firstName;
   }
 
  
   public void setName(String name) {
       this.name = name;
       notifyPropertyChanged(BR.name);
   }
}

@Bindable 在编译时将生成一个BR class 。BR类文件将生成在moudle的package下。如果数据类的基类不能被改变,
Observable interface的实现类可以使用PropertyChangeRegistry存储和有效地通知listeners。


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

}


 (2)观察属性(ObservableFields)

 使用ObservableField 和它的同级的一些类型: 
 
 ObservableBoolean, ObservableByte, ObservableChar, ObservableShort, ObservableInt,
 ObservableLong, ObservableFloat, ObservableDouble, and ObservableParcelable。
 


  (3) 观察集合(Observable Collections)

   ObservableArrayMap<String, Object> mObservableArrayMap = new ObservableArrayMap<>();
   mObservableArrayMap.put(xx,xx);


   ObservableArrayList<Object> mObservableArrayList = new ObservableArrayList<>();



   <data>
    <import type="android.databinding.ObservableMap"/>
    <variable name="user" type="ObservableMap<String, Object>"/>

   </data>



 //view model 中的定义
 public class MainModel{
 

    public BaseObservable mBaseObservable = new TestBean();

    public ObservableField<String> firstName = new ObservableField<>()
 
    public ObservableList<List> firstName = new ObservableList<>()
 
    public final ObservableInt age = new ObservableInt();
 
 
 }

生成的绑定类,会链接布局的variables。绑定的名称和包可能是自定义的的(生成的所有绑定类都 extends ViewDataBinding)

 生成
 MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater);
 MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater, viewGroup, false);

 
 ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater, layoutId, parent, attachToParent);
 ViewDataBinding binding = DataBindingUtil.bindTo(viewRoot, layoutId);


 单独绑定
 MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot);

高级绑定

动态Variable
有时,特定的绑定类不会为人所知。例如,一个RecyclerView.Adapter操作的任意布局,不知其特定的绑定类。仍需要通过onBindViewHolder(VH, int)来绑定值。 
在下面的例子中,RecyclerView中的所有布局,都绑定了一个”item”,有一个BindingHolder#getBinding() 返回ViewDataBinding 类型


直接绑定

当一个variable or observable发生了改变,绑定框架会安排在下一帧进行视图的改变。
然而,有时希望立即发生改变。可以使用executePendingBindings()来强制执行

public void onBindViewHolder(BindingHolder holder, int position) {
   final T item = mItems.get(position);
   //xml 中的 item 塞入值
   holder.getBinding().setVariable(BR.item, item);
   //强制执行
   holder.getBinding().executePendingBindings();
}



Attribute Setters  (主要是 注解相关知识)

当view使用了绑定表达式,只要绑定值发生变化,生成的绑定类必须调用相应的setter方法。定制数据绑定框架的方式方法调用设置值。数据绑定框架允许自定义setter方法。

(1)自动Setters +  自定义Setters(  自定义属性 )


自动Setters

对于一个attribute,数据绑定框架将试图查找对应的setAttribute()。它的namespace并不重要,只关注attribute的name。
如,TextView中的属性android:text使用了表达式,那么框架就会查找setText(String)。如果表达式返回的是int,那么将会查找setText(int)。
所以,表达式需要返回正确的类型。数据绑定框架,支持创建一个布局元素(View|ViewGroup)中,并不存在的属性( 用于自定义属性)。 
如下,生成的binding class中,将生成一个setDrawerListener(listener):

自定义Setters

一些属性需要自定义绑定逻辑。例如,属性android:paddingLeft没有对应的setter方法,
而在view中有一个方法为setPadding(left, top, right, bottom)  
可以使用@ BindingAdapter来自定义一个关于属性android:paddingLeft的setter。
@BindAdapter这个相当于注解这个方法是一个自定的属性;{}内部表示使用时这个属性的名字,比如说@BindAdapter(“{image_url}”),
在xml中可以这样子使用app:image_url=”url”,注意这些自定属性是在http://schemas.android.com/apk/res-auto这个命名空间下的(也就是data binding的空间下),
记得声明;声明自定义属性的方法名是不限制的,遵从Java方法的规范,可以重载等,data binding会针对对应使用的地方,
找到相匹配的方法进行设置;方法的参数,第一个必须是View的子类,第二个也就是需要传进来的数据,可以是任意的数据类型,在xml中也可以用databinding的实体传递进来。

例如:

重命名 Setters

有一些属性的 setter方法, 与它的属性名不匹配。 这些属性的setter可能会 通 过 使用 @BindingMethods,
来进行重命名。 要求内问包 含 @BindingMethod。 例如ImageView有一 个属性android:tint,它对应setter方法为setImageTintList(ColorStateList tint):


@BindingMethods({
       @BindingMethod(type = "android.widget.ImageView",
                      attribute = "android:tint",
                      method = "setImageTintList"),
})
开发人员将不太可能需要重命名setter,因android属性框架已经实现。

自定义Setters
<layout http://schemas.android.com/apk/res-auto

<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:scrimColor="@{@color/scrim}"
    app:drawerListener="@{fragment.drawerListener}"
    />


<com.test.MyView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:scrimColor="@{@color/scrim}"
    app:drawerListener="@{fragment.drawerListener}"
    />
/>


创建binding类(自定义Setters)

public class MyViewBinding{


      /** 自定义自己的属性,网上copy的 **/

      @BindingAdapter({"bind:name", "bind:age"}) //看到网上有bind:name 这种,不过没用过
      public static void myNameAge(View view, String name,String age) {
	 view.setText();
      }

     @BindingAdapter({"image_url"})
     public static void loadImage(ImageView view, String url){
        if(!StringUtils.isEmpty(url)) {
            Glide.with(view.getContext()).load(url).into(view);  //设置全缓存.diskCacheStrategy(DiskCacheStrategy.ALL)
        }
     }

     @BindingAdapter({"image_url"})
     public static void loadImage(ImageView view, int resoureId){
        view.setImageResource(resoureId);
     }

     @BindingAdapter({"image_url"})
     public static void loadImage(DraweeView view, String url){
        view.setController(Fresco.newDraweeControllerBuilder().setUri(url).build());
     }
 
     @BindingAdapter({"weight"})
     public static void setWeight(View view, int weight){
        if(weight < 0 ){
            weight = 0 ;
        }
        view.setLayoutParams(new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT,
                weight));
     }

     @BindingAdapter({"imageWidth"})
     public static void setImageWidth(ImageView view, float size){
        ViewGroup.LayoutParams params = view.getLayoutParams();
        params.width = UIUtils.dp2Px(size);
        view.setLayoutParams(params);
     }

     @BindingAdapter({"imageHeight"})
     public static void setImageHeight(ImageView view, float size){
        ViewGroup.LayoutParams params = view.getLayoutParams();
        params.height = UIUtils.dp2Px(size);
        view.setLayoutParams(params);
     }

     @BindingAdapter({"RLMargins"})
     public static void setRLMargins(View view, MarginInfo marginInfo){
        if (marginInfo == null){
            return;
        }
        RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams( ViewGroup.LayoutParams
                .WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
        layoutParams.setMargins(marginInfo.getLeft(),marginInfo.getTop(),marginInfo.getRight(),
                marginInfo.getBottom());
        view.setLayoutParams(layoutParams);
     }

     @BindingAdapter({"LLMargins"})
     public static void setLLMargins(View view, MarginInfo marginInfo){
        if (marginInfo == null){
            return;
        }
        LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams( ViewGroup.LayoutParams
                .WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
        layoutParams.setMargins(marginInfo.getLeft(),marginInfo.getTop(),marginInfo.getRight(),
                marginInfo.getBottom());
        view.setLayoutParams(layoutParams);
     }


      //自定义布局绑定(可用于listview等)
      @BindingAdapter({"bind:itemView")
      public static void myNameAge(ListView view, itemBinder<T>  itemViewWarp) {
	 Adapter aa = new Adapter(itemViewWarp)
         view.setAdapetr(aa);
      }



      /** 对系统的 paddingLeft 进行自己转换操作 **/
      @BindingAdapter("android:paddingLeft")
      public static void myName(View view, int padding) {
        view.setPadding(padding,
                   view.getPaddingTop(),
                   view.getPaddingRight(),
                   view.getPaddingBottom());
      }
}
转换器

对象转换

当从一个绑定表达式返回一个对象,就会有一个setter被采用。
该对象将会被转换成setter中的参数类型。 

下面的例子,通过ObservableMaps来保存数据:

userMap返回一个对象,该对象将自动转换成setText(CharSequence)中的参数类型。
可能会有混乱的参数类型,开发人员需要在表达式中显式cast

<TextView
   android:text='@{String.valueOf(userMap["lastName"])}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>



自定义转换

有时特定类型之间会自动转换。例如,设置背景:

<View
   android:background="@{isError ? @color/red : @color/white}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>


这里,background的setter的参数类型应该是一个drawable,但color是一个整数。
应当有一个转换规则,将int color转换为Col orDrawable。
这种转换是通过使用一个 @BindingConversion的静态方法来实现的:


//此方法默认已实现,所以可以默认不用管
@BindingConversion 
public static ColorDrawable convertColorToDrawable(int color) {
   return new ColorDrawable(color);
}

注意,转换只发生在setter时期。所以不允许混合类型,如:

<View
   android:background="@{isError ? @drawable/error : @color/white}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>
...





  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

空白的泡

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值