引言
布局(Layout)和视图(View)
当进行Android应用程序开发时,布局(Layout)和视图(View)是两个核心概念。它们在Android界面设计和用户界面开发中起着重要的角色。
视图( View ) | 布局( Layout ) | 布局属性( Layout Attributes ) | |
|---|---|---|---|
| 定义 | - 视图是Android用户界面的基本构建块 | - 定义布局是指在屏幕上排列和组织视图的方式。 | - 布局属性是用于指定视图在布局中的行为和特性的属性 |
| 说明 | - 视图是可见元素,用于在屏幕上呈现信息和与用户进行交互。例如,按钮、文本框、图像、复选框等都是视图的示例; |
- 每个视图都有自己的属性和行为,可以通过编程方式进行操作和定制。 | - 布局决定了视图在屏幕上的位置、大小和相对关系。
- 在Android中,布局通过XML文件或代码来定义。
- 布局可以是线性布局(
LinearLayout)、相对布局(RelativeLayout)、帧布局(FrameLayout)等等。 - 布局可以嵌套,创建复杂的层次结构来实现灵活的界面设计。 | - 通过布局属性,可以控制视图的大小、位置、对齐方式等。例如,通过布局属性,您可以设置视图的宽度、高度、外边距、内边距、对齐方式等;
- 布局属性可以通过XML文件或代码进行设置和定制。 |
综上所述,布局和视图是Android应用程序开发中的重要概念。视图表示用户界面的可见元素,而布局用于组织和排列视图。布局属性则控制视图在布局中的行为和特性
什么是LayoutInflater
上面我们已经了解了View和Layout的概念,而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();// 第三种方式
-
通过
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE)获取LayoutInflater -
通过
LayoutInflater.from(context)获取LayoutInflater -
通过
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的概念在juejin.cn/post/725850… ,Activity继承自ContextThemeWrapper,Application和Service则继承自ContextWrapper,具体继承关系为:
对于上述几种Context,getSystemService方法实现主要的区分在于ContextImpl与ContextThemeWrapper。
ContextImpl.getSystemService
ContextImpl中getSystemService的实现如下:
@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对象,用于替换LayoutInflater中mContext变量:
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();
}
小结
-
Context.getSystemService主要有两种不同实现,一种是ContextImpl的实现:直接新建PhoneLayoutInflater对象,另一种是ContextThemeWrapper的实现:通过getBaseContext(通常是ContextImpl对象)新建PhoneLayoutInflater对象,接着clone中一个新的PhoneLayoutInflater对象,并将其中的mContext替换为ContextThemeWrapper; -
不同的
Context实例会新建出不同的LayoutInflater对象
inflate()方法
聊完了如何获取LayoutInflater对象之后,接下来就可以探究在LayoutInflater 的infalte方法
常用的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();
}
}
-
该函数内部会首先通过
tryInflatePrecompiled函数判断是否有预编译的View对象,这是Android10新增的一个优化,将XmlResourceParser解析XML的放在编译时期,减少运行时该部分消耗的时间,从而缩短inflate的时间; -
如果没有预编译的
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分支的代码即可:
-
创建XML的根
View; -
如果参数
root不为空,则会根据根View的属性创建LayoutParamsparams,当attachToRoot为false时,将params赋值给根View -
加载所有子
View; -
如果参数
root不为空并且attachToRoot为true,则调用root.addView; -
如果参数
root为空或者attachToRoot为false,则返回当前XML的根View,否则返回root
根据上述代码,我们可以根据root与attachToRoot两者的值来分析inflate结果,inflate结果包括两个方面:
-
返回的结果时XML根节点
View还是root; -
XML中的根节点
View是否有对应的LayoutParams
root:View | attachToRoot: Boolen | 返回的结果 | 根节点 View 是否有对应的 LayoutParams |
|---|---|---|---|
null | false | XML的根节点View | 否 |
null | true | XML的根节点View | 否 |
NotNull | false | XML的根节点View | 是 |
NotNull | true | root | 是 |
小结
当我们传入的root与attachToRoot值不同时,inflate返回的结果一级根节点View是否包含对应的LayoutParams 是不同的
添加自定义View示例
为了测试上文中Inflate的知识点,我们举几个🌰来看一下传入的root与attachToRoot值不同时,View会有什么结果。
- 新建自定义
View:layout_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>
- 在
MainActivity中inflate自定义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为空,attachToRoot为false时:返回没有LayoutParams的根节点View
val customView = layoutInflater.inflate(R.layout.layout_custom_view, null)
可以看到,该自定义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为空,attachToRoot为true时:返回没有LayoutParams的根节点View
结果和上一种情况一致
root不为空,attachToRoot为false时:返回有对应LayoutParams的根节点View
该结果和自定义View的样式完全一致。
root不为空,attachToRoot为true时:返回有对应LayoutParams的root
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不为空,attachToRoot为true时,返回的时root,而该root已经有parentView,不能再次作为其他View的子View
总结
-
获取
LayoutInflater时,不同的Context会得到不同的LayoutInflater对象,ContextThemeWrapper中会clone新的PhoneLayoutInflater,并将自己赋值给为该对象中的context属性; -
inflate方法中的root与attachToRoot参数在不同值的情况下会得到不同的结果,root最好不要为null,否则根节点的宽高设置不会生效
补充
在Fragment的OnCreateView,以及在RecyclerView.Adapter的onCreateViewHolder中调用inflate时,parent不要为null,否则宽高设置不会生效,attachToRoot值一定不要设置为true,否则会崩溃。