Android深入学习之LayoutInflater类和ViewBinding

在build.gradle(Module)中添加viewBinding元素后,Android会自动给模块中的每个XML布局文件生成一个相应的Binding类,该Binding类名称为XML布局文件驼峰式大写+Binding后缀。以如下所示的activity_welcome.xml文件为例,对应的ActivityWelcomeBinding.java的源代码如下所示。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/welcome_container"
    android:orientation="vertical"
    android:gravity="center"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Please choose an item"
        android:textSize="30dp"
        android:gravity="center"/>
    <LinearLayout
        android:id="@+id/button_container"
        android:layout_marginTop="30dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical">
        <Button
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="GEOGRAPHY"
            android:id="@+id/btn_geo"/>
        <Button
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="MATHEMATICS"
            android:id="@+id/btn_math"/>
    </LinearLayout>
</LinearLayout>
activity_welcome.xml

// Generated by view binder compiler. Do not edit!
package com.larissa.android.quiz.databinding;

import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.viewbinding.ViewBinding;
import androidx.viewbinding.ViewBindings;
import com.larissa.android.quiz.R;
import java.lang.NullPointerException;
import java.lang.Override;
import java.lang.String;

// ActivityWelcomeBinding是final类,实现ViewBinding接口
public final class ActivityWelcomeBinding implements ViewBinding {
  // XML布局文件中的根元素被定义为一个私有的常量字段
  @NonNull
  private final LinearLayout rootView;
  // XML布局文件中有id的控件被定义为公有的常量字段
  @NonNull
  public final Button btnGeo;

  @NonNull
  public final Button btnMath;

  @NonNull
  public final LinearLayout buttonContainer;

  @NonNull
  public final LinearLayout welcomeContainer;

  // ActivityWelcomeBinding的构造函数为私有的,即ActivityWelcomeBinding不能在类的外部通过new实例化
  // 构造函数的参数为上方所定义的常量字段
  private ActivityWelcomeBinding(@NonNull LinearLayout rootView, @NonNull Button btnGeo,
      @NonNull Button btnMath, @NonNull LinearLayout buttonContainer,
      @NonNull LinearLayout welcomeContainer) {
    this.rootView = rootView;
    this.btnGeo = btnGeo;
    this.btnMath = btnMath;
    this.buttonContainer = buttonContainer;
    this.welcomeContainer = welcomeContainer;
  }

  // 实现ViewBinding接口中定义的getRoot()方法,其实这就是rootView字段的getter方法
  @Override
  @NonNull
  public LinearLayout getRoot() {
    return rootView;
  }

  // 返回ActivityWelcomeBinding实例的两个inflate()方法。可见,inflate(LayoutInflater)方法
  // 其实是调用了inflate(LayoutInflater, ViewGroup, boolean)方法
  @NonNull
  public static ActivityWelcomeBinding inflate(@NonNull LayoutInflater inflater) {
    return inflate(inflater, null, false);
  }

  @NonNull
  public static ActivityWelcomeBinding inflate(@NonNull LayoutInflater inflater,
      @Nullable ViewGroup parent, boolean attachToParent) {
    // 调用LayoutInflater.inflate()方法,返回解析XML布局文件得到的View对象/View树
    View root = inflater.inflate(R.layout.activity_welcome, parent, false);
    // 如果attachToParent为true,则将View对象贴parent上
    if (attachToParent) {
      parent.addView(root);
    }
    return bind(root);
  }

  // bind()方法其实是获取ActivityWelcomeBinding实例的最终方法
  @NonNull
  public static ActivityWelcomeBinding bind(@NonNull View rootView) {
    // The body of this method is generated in a way you would not otherwise write.
    // This is done to optimize the compiled bytecode for size and performance.
    int id;
    missingId: {
      id = R.id.btn_geo;
      // 从rootView树中查找指定id的控件
      Button btnGeo = ViewBindings.findChildViewById(rootView, id);
      // 如果从rootView树中没有找到指定id的控件,则跳出missingId label
      if (btnGeo == null) {
        break missingId;
      }

      id = R.id.btn_math;
      Button btnMath = ViewBindings.findChildViewById(rootView, id);
      if (btnMath == null) {
        break missingId;
      }

      id = R.id.button_container;
      LinearLayout buttonContainer = ViewBindings.findChildViewById(rootView, id);
      if (buttonContainer == null) {
        break missingId;
      }

      LinearLayout welcomeContainer = (LinearLayout) rootView;
      // 所有的控件都找到了,则实例化ActivityWelcomeBinding对象
      return new ActivityWelcomeBinding((LinearLayout) rootView, btnGeo, btnMath, buttonContainer,
          welcomeContainer);
    }
    // 有控件没有找到的话,则抛出错误
    String missingId = rootView.getResources().getResourceName(id);
    throw new NullPointerException("Missing required view with ID: ".concat(missingId));
  }
}
ActivityWelcomeBinding.java

可见,在ActivityWelcomeBinding中用到了LayoutInflater类。LayoutInflater类中也定义了inflate()方法,该方法的主要作用就是将XML布局文件解析为相应的View对象或者称为View树,可实现动态换肤、视图转换、属性转换等需求。activity_welcome.xml对应的View树如下图所示。

LayoutInflater是一个抽象类,在Activity中获得与其相关联的LayoutInflater实例一般是通过调用Activity.getLayoutInflater()方法。关于Activity.getLayoutInflater()方法是如何得到与Activity相关联的LayoutInflater的实例请参见:https://www.jianshu.com/p/a4dd4892c84e

LayoutInflater类中定义了4个inflate()方法:

1.public View inflate (int resource, ViewGroup root)

2.public View inflate(XmlPullParser parser, ViewGroup root)

3.public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot)

4.public View inflate(int resource, ViewGroup root, boolean attachToRoot)

通过源代码可以发现,这些inflate()其实最终都调用了第3个inflate()方法。本文并不打算对LayoutInflater.inflate()方法的业务逻辑进行详细分析,只是通过源代码其实可以发现,在inflate()中其实会调用到LayoutInflater.createView()方法,而在createView()方法中其实是用反射reflection来动态地生成View对象的。

另外,值得一提的是,在第3个和第4个inflate()方法中,当attachToRoot为true时,返回的是ViewGroup root对象;当attachToRoot为false时,返回的是解析resource得到的View对象。这里可以看个例子:

public class WelcomeActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_welcome);
        View rootView=getLayoutInflater().inflate(R.layout.activity_welcome,null,false);
        getAllViews(rootView);
    }
    private void getAllViews(View view){
        if(view instanceof ViewGroup){
            Log.d(TAG,"ViewGroup:".concat(view.getClass().getTypeName()));
            int count=((ViewGroup) view).getChildCount();
            for(int i=0;i<count;i++){
                getAllViews(((ViewGroup) view).getChildAt(i));
            }
        }
        else{
            Log.d(TAG,"View:".concat(view.getClass().getTypeName()));
        }
    }
}

当ViewGroup为null,且attachToRoot为false时,日志输出为:

而当指定了一个FrameLayout作为ViewGroup,且attachToRoot为true时,日志输出为:

public class WelcomeActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_welcome);
        FrameLayout viewGroup=new FrameLayout(this);
        View rootView=getLayoutInflater().inflate(R.layout.activity_welcome,viewGroup,true);
        getAllViews(rootView);
    }
    private void getAllViews(View view){
        if(view instanceof ViewGroup){
            Log.d(TAG,"ViewGroup:".concat(view.getClass().getTypeName()));
            int count=((ViewGroup) view).getChildCount();
            for(int i=0;i<count;i++){
                getAllViews(((ViewGroup) view).getChildAt(i));
            }
        }
        else{
            Log.d(TAG,"View:".concat(view.getClass().getTypeName()));
        }
    }
}

故而可以发现,ViewBinding.inflate()和LayoutInflater.inflate()是不同的。在ViewBinding.inflate()中,即便指定了ViewGroup且attachToParent为true,但是返回的仍然是解析XML布局文件得到的View树。只不过当attachToParent为true时,解析XML布局文件得到的View树被贴在了ViewGroup parent(形参)上,那么对应的实参也会被修改。


通过分析ActivityWelcomeBinding源代码可知,在WelcomeActivity中使用ViewBinding首先是调用inflate()方法得到ActivityWelcomeBinding的实例,然后调用ActivityWelcomeBinding.getRoot()方法得到rootView,并通过setContentView()注入rootView。进而在WelcomeActivity中就可以通过ActivityWelcomeBinding的实例.属性名的方式得到相应的控件并使用控件。如下方代码所示:

public class WelcomeActivity extends AppCompatActivity {
    public static final String CLASS="class";
    public static final int GEOGRAPHY=0;
    public static final int MATHEMATICS=1;
    private final String TAG="Welcome";
    private ActivityWelcomeBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        getSupportActionBar().setTitle("Welcome to Quiz");
        binding=ActivityWelcomeBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());
        // btnGeo是ActivityWelcomeBinding中定义的公有字段
        binding.btnGeo.setOnClickListener(v->btnClick(GEOGRAPHY));
        binding.btnMath.setOnClickListener(v->btnClick(MATHEMATICS));
    }

    private void btnClick(int flag){
        Intent intent= new Intent(this,MainActivity.class);
        intent.putExtra(CLASS,flag);
        startActivity(intent);
    }
}

 那么如果代码改成下方这样,WelcomeActivity的屏幕则如下图所示。ActivityWelcomeBinding.inflate()返回的仍然是ActivityWelcomeBinding的实例,但是改变了FrameLayout parent这个实参。

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        getSupportActionBar().setTitle("Welcome to Quiz");
        FrameLayout parent=new FrameLayout(this);
        TextView msg=new TextView(this);
        msg.setText("FrameLayout is the parent");
        msg.setTextSize(30);
        parent.addView(msg);
        binding=ActivityWelcomeBinding.inflate(getLayoutInflater(),parent,true);
        setContentView(parent);
        // btnGeo是ActivityWelcomeBinding中定义的公有字段
        binding.btnGeo.setOnClickListener(v->btnClick(GEOGRAPHY));
        binding.btnMath.setOnClickListener(v->btnClick(MATHEMATICS));
    }

posted @ 2023-04-16 15:24  南风小斯  阅读(151)  评论(0编辑  收藏  举报