导言
布局(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的概念在/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,不然会崩溃。




