导言

布局(Layout)和视图(View)

当进行Android应用程序开发时,布局(Layout)和视图(View)是两个中心概念。它们在Android界面设计和用户界面开发中起着重要的角色。

视图( View ) 布局( Layout ) 布局特点( Layout Attributes )
界说 – 视图是Android用户界面的基本构建块 – 界说布局是指在屏幕上摆放和安排视图的办法。 – 布局特点是用于指定视图在布局中的行为和特性的特点
阐明 – 视图是可见元素,用于在屏幕上出现信息和与用户进行交互。例如,按钮、文本框、图像、复选框等都是视图的示例;
  • 每个视图都有自己的特点和行为,能够经过编程办法进行操作和定制。 | – 布局决议了视图在屏幕上的方位、巨细和相对联系。
  • 在Android中,布局经过XML文件或代码来界说。
  • 布局能够是线性布局(LinearLayout)、相对布局(RelativeLayout)、帧布局(FrameLayout)等等。
  • 布局能够嵌套,创立复杂的层次结构来完结灵敏的界面设计。 | – 经过布局特点,能够操控视图的巨细、方位、对齐办法等。例如,经过布局特点,您能够设置视图的宽度、高度、外边距、内边距、对齐办法等;
  • 布局特点能够经过XML文件或代码进行设置和定制。 |

综上所述,布局和视图是Android应用程序开发中的重要概念。视图表明用户界面的可见元素,而布局用于安排和摆放视图。布局特点则操控视图在布局中的行为和特性

什么是LayoutInflater

上面咱们现已了解了ViewLayout的概念,而LayoutInflater是Android中用于将布局资源文件(XML)实例化为相应的视图目标的工具,翻译成中文是布局加载器。

经过LayoutInflater,能够将预界说的XML布局文件转换为实践的视图目标,这些目标能够在屏幕上显示并与用户进行交互。

获取LayoutInflater

在Android开发中,能够经过以下三种办法获取LayoutInflater

LayoutInflater inflater =
(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);// 第一种办法
LayoutInflater inflater = LayoutInflater.from(context);// 第二种办法
LayoutInflater inflater = activity.getLayoutInflater();// 第三种办法
  1. 经过context.getSystemService(Context.LAYOUT_INFLATER_SERVICE)获取LayoutInflater

  2. 经过LayoutInflater.from(context)获取LayoutInflater

  3. 经过activity.getLayoutInflater()获取LayoutInflater

而第二种办法:LayoutInflater.from(context)的源码:

public static LayoutInflater from(@UiContext Context context) {
    LayoutInflater LayoutInflater =
            (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    if (LayoutInflater == null) {
        throw new AssertionError("LayoutInflater not found.");
    }
    return LayoutInflater;
}

from办法中也是调用context.getSystemService办法,所以实践上第二种办法也只是第一种办法的包装

activity.getLayoutInflater()

剖析activity.getLayoutInflater,咱们直接从源码剖析:

public LayoutInflater getLayoutInflater() {
    return getWindow().getLayoutInflater();
}

activity.getLayoutInflater实践调用的是Window.getLayoutInflater()办法:

public abstract LayoutInflater getLayoutInflater();

Window是一个抽象类,具体完结类是PhoneWindow,咱们查看PhoneWindow.getLayoutInflater办法:

public LayoutInflater getLayoutInflater() {
    return mLayoutInflater;
}

直接回来mLayoutInflater变量,而mLayoutInflater是在初始化时进行赋值的:

public PhoneWindow(@UiContext Context context) {
    super(context);
    mLayoutInflater = LayoutInflater.from(context);
    mRenderShadowsInCompositor = Settings.Global.getInt(context.getContentResolver(),
            DEVELOPMENT_RENDER_SHADOWS_IN_COMPOSITOR, 1) != 0;
    mProxyOnBackInvokedDispatcher = new ProxyOnBackInvokedDispatcher(
            context.getApplicationInfo().isOnBackInvokedCallbackEnabled());
}

能够看到,终究仍是调用LayoutInflater.from(context)办法获取LayoutInflater,也便是说一切的获取LayoutInflater办法其实都是经过context.getSystemService(Context.LAYOUT_INFLATER_SERVICE)获取LayoutInflater区别只在于context中不同的完结。

context.getSystemService

Context的概念在/post/725850… ,Activity承继自ContextThemeWrapperApplicationService则承继自ContextWrapper,具体承继联系为:

聊聊LayoutInflater以及inflate方法

对于上述几种ContextgetSystemService办法完结首要的区别在于ContextImplContextThemeWrapper

ContextImpl.getSystemService

ContextImplgetSystemService的完结如下:


@Override
public Object getSystemService(String name) {
    if (vmIncorrectContextUseEnabled()) {
        // Check incorrect Context usage.
        if (WINDOW_SERVICE.equals(name) && !isUiContext()) {
            final String errorMessage = "Tried to access visual service "
                    + SystemServiceRegistry.getSystemServiceClassName(name)
                    + " from a non-visual Context:" + getOuterContext();
            final String message = "WindowManager should be accessed from Activity or other "
                    + "visual Context. Use an Activity or a Context created with "
                    + "Context#createWindowContext(int, Bundle), which are adjusted to "
                    + "the configuration and visual bounds of an area on screen.";
            final Exception exception = new IllegalAccessException(errorMessage);
            StrictMode.onIncorrectContextUsed(message, exception);
            Log.e(TAG, errorMessage + " " + message, exception);
        }
    }
    return SystemServiceRegistry.getSystemService(this, name);
}

咱们在SystemServiceRegistry中能够找到服务注册的地方:

registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class,
        new CachedServiceFetcher<LayoutInflater>() {
    @Override
    public LayoutInflater createService(ContextImpl ctx) {
        return new PhoneLayoutInflater(ctx.getOuterContext());
    }});

这儿回来LayoutInflater的完结类PhoneLayoutInflater ,构造函数中包括Context参数

ContextThemeWrapper.getSystemService

ContextThemeWrapper.getSystemService中的完结有所不同:

@Override
public Object getSystemService(String name) {
    if (LAYOUT_INFLATER_SERVICE.equals(name)) {
        if (mInflater == null) {
            mInflater = LayoutInflater.from(getBaseContext()).cloneInContext(this);
        }
        return mInflater;
    }
    return getBaseContext().getSystemService(name);
}

能够看到,ContextThemeWrapper中首先会获取PhoneLayoutInflater ,然后调用cloneInContext新建了一个PhoneLayoutInflater目标:

public LayoutInflater cloneInContext(Context newContext) {
    return new PhoneLayoutInflater(this, newContext);
}

在新的PhoneLayoutInflater目标中会传入新的Context目标,即ContextThemeWrapper目标,用于替换LayoutInflatermContext变量:

protected LayoutInflater(LayoutInflater original, Context newContext) {
    StrictMode.assertConfigurationContext(newContext, "LayoutInflater");
    mContext = newContext;
    mFactory = original.mFactory;
    mFactory2 = original.mFactory2;
    mPrivateFactory = original.mPrivateFactory;
    setFilter(original.mFilter);
    initPrecompiledViews();
}

小结

  1. Context.getSystemService首要有两种不同完结,一种是ContextImpl的完结:直接新建PhoneLayoutInflater目标,另一种是ContextThemeWrapper的完结:经过getBaseContext(通常是ContextImpl目标)新建PhoneLayoutInflater目标,接着clone中一个新的PhoneLayoutInflater目标,并将其中的mContext替换为ContextThemeWrapper

  2. 不同的Context实例会新建出不同的LayoutInflater目标

inflate()办法

聊完了怎么获取LayoutInflater目标之后,接下来就能够探究在LayoutInflaterinfalte办法

常用的inflate办法有两个:

// 必传参数XML id,可选参数根View
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
    return inflate(parser, root, root != null);
}

这个办法接纳两个参数,第一个参数是布局文件的资源ID(例如R.layout.my_layout),第二个参数是父View,表明生成的View将会被增加到该父View中,终究也是调用下面的办法

//  必传参数XML id,可选参数根View, 
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)

这个办法与前面的办法类似,但多了一个boolean类型的参数attachToRoot。假如该参数为true,则生成的View将主动增加到root中,假如为false,则不会主动增加,需要手动增加到父View中。

因为上述两个办法终究都是经过第二个办法完结调用,因而,咱们直接看第二个办法的完结:

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
    final Resources res = getContext().getResources();
    if (DEBUG) {
        Log.d(TAG, "INFLATING from resource: "" + res.getResourceName(resource) + "" ("
              + Integer.toHexString(resource) + ")");
    }
    View view = tryInflatePrecompiled(resource, res, root, attachToRoot);
    if (view != null) {
        return view;
    }
    XmlResourceParser parser = res.getLayout(resource);
    try {
        return inflate(parser, root, attachToRoot);
    } finally {
        parser.close();
    }
}
  1. 该函数内部会首先经过tryInflatePrecompiled函数判别是否有预编译的View目标,这是Android10新增的一个优化,将XmlResourceParser解析XML的放在编译时期,削减运转时该部分耗费的时刻,从而缩短inflate的时刻;

  2. 假如没有预编译的View目标,则会调用inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) 办法

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
    synchronized (mConstructorArgs) {
        final Context inflaterContext = mContext;
        final AttributeSet attrs = Xml.asAttributeSet(parser);
        Context lastContext = (Context) mConstructorArgs[0];
        mConstructorArgs[0] = inflaterContext;
        View result = root;
        try {
            advanceToRootNode(parser);
            final String name = parser.getName();
            if (TAG_MERGE.equals(name)) {
                if (root == null || !attachToRoot) {
                    throw new InflateException("<merge /> can be used only with a valid "
                            + "ViewGroup root and attachToRoot=true");
                }
                rInflate(parser, root, inflaterContext, attrs, false);
            } else {
                // 1. 创立XML的根View
                // Temp is the root view that was found in the xml
                final View temp = createViewFromTag(root, name, inflaterContext, attrs);
                ViewGroup.LayoutParams params = null;
                //2. 假如参数root不为空,则会依据根View的特点创立LayoutParams
                if (root != null) {
                    // Create layout params that match root, if supplied
                    params = root.generateLayoutParams(attrs);
                    if (!attachToRoot) {
                        // Set the layout params for temp if we are not
                        // attaching. (If we are, we use addView, below)
                        temp.setLayoutParams(params);
                    }
                }
                // 3. 加载一切子View
                // Inflate all children under temp against its context.
                rInflateChildren(parser, temp, attrs, true);
                // 4. 假如参数root不为空而且attachToRoot为true,则调用root.addView
                // We are supposed to attach all the views we found (int temp)
                // to root. Do that now.
                if (root != null && attachToRoot) {
                    root.addView(temp, params);
                }
                // 5. 假如参数root为空或许attachToRoot为false,则回来当时XML的根View,不然回来root
                // Decide whether to return the root that was passed in or the
                // top view found in xml.
                if (root == null || !attachToRoot) {
                    result = temp;
                }
            }
        return result;
    }
}

这儿判别XML的根布局是否是<merge>标签,咱们这儿的剖析暂时不考虑<merge>标签,因而咱们看else分支的代码即可:

  1. 创立XML的根View

  2. 假如参数root不为空,则会依据根View的特点创立LayoutParams params,当attachToRootfalse时,将params 赋值给根View

  3. 加载一切子View

  4. 假如参数root不为空而且attachToRoottrue,则调用root.addView

  5. 假如参数root为空或许attachToRootfalse,则回来当时XML的根View,不然回来root

依据上述代码,咱们能够依据rootattachToRoot两者的值来剖析inflate成果,inflate成果包括两个方面:

  1. 回来的成果时XML根节点View仍是root

  2. XML中的根节点View是否有对应的LayoutParams

root:View attachToRoot: Boolen 回来的成果 根节点 View 是否有对应的 LayoutParams
null false XML的根节点View
null true XML的根节点View
NotNull false XML的根节点View
NotNull true root

小结

当咱们传入的rootattachToRoot值不一起,inflate回来的成果一级根节点View是否包括对应的LayoutParams 是不同的

增加自界说View示例

为了测验上文中Inflate的知识点,咱们举几个来看一下传入的rootattachToRoot值不一起,View会有什么成果。

  1. 新建自界说Viewlayout_custom_view.xml,宽度match_parent,高度为200dp
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="200dp"
    android:background="#FF1"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Custom View"
        android:textSize="40dp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
  1. MainActivityinflate自界说View,并增加到activity_main布局中:
class MainActivity : AppCompatActivity() {
    private val binding: ActivityMainBinding by lazy {
        ActivityMainBinding.inflate(layoutInflater)
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)
        val customView = layoutInflater.inflate(R.layout.layout_custom_view,         binding.root, false)
        binding.root.addView(customView)
    }
}

咱们依照上文四种状况测验:

  • root为空,attachToRootfalse时:回来没有LayoutParams的根节点View
val customView = layoutInflater.inflate(R.layout.layout_custom_view, null)

聊聊LayoutInflater以及inflate方法

能够看到,该自界说View的宽高并没有依照根节点设置的值,符合咱们的预期,但View的宽高看上去时依照WRAP_CONTENT的值进行设置的,这是为什么?咱们能够看一下addView的源码:

public void addView(View child, int index) {
    if (child == null) {
        throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
    }
    LayoutParams params = child.getLayoutParams();
    if (params == null) {
        params = generateDefaultLayoutParams();
        if (params == null) {
            throw new IllegalArgumentException(
                    "generateDefaultLayoutParams() cannot return null  ");
        }
    }
    addView(child, index, params);
}

当子View没有LayoutParams时,会调用generateDefaultLayoutParams()生成默许的LayoutParams

// android.view.ViewGroup#generateDefaultLayoutParams
protected LayoutParams generateDefaultLayoutParams() {
    return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}

能够看到,这儿默许的LayoutParams中,宽高便是WRAP_CONTENT

  • root为空,attachToRoottrue时:回来没有LayoutParams的根节点View

聊聊LayoutInflater以及inflate方法

成果和上一种状况一致

  • root不为空,attachToRootfalse时:回来有对应LayoutParams的根节点View

聊聊LayoutInflater以及inflate方法

该成果和自界说View的样式完全一致。

  • root不为空,attachToRoottrue时:回来有对应LayoutParamsroot
java.lang.RuntimeException: Unable to start activity         ComponentInfo{com.example.inflatedemo/com.example.inflatedemo.MainActivity}: java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first.
                                                                                        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:4324)
Caused by: java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first.
                                                                                       at android.view.ViewGroup.addViewInner(ViewGroup.java:5487)
at android.view.ViewGroup.addView(ViewGroup.java:5316)

运转直接报错,这是因为在root不为空,attachToRoottrue时,回来的时root,而该root现已有parentView,不能再次作为其他View的子View

总结

  1. 获取LayoutInflater时,不同的Context会得到不同的LayoutInflater目标,ContextThemeWrapper中会clone新的PhoneLayoutInflater,并将自己赋值给为该目标中的context特点;

  2. inflate办法中的rootattachToRoot参数在不同值的状况下会得到不同的成果,root最好不要为null,不然根节点的宽高设置不会收效

弥补

FragmentOnCreateView,以及在RecyclerView.AdapteronCreateViewHolder中调用inflate时,parent不要为null,不然宽高设置不会收效,attachToRoot值必定不要设置为true,不然会崩溃。