导言
布局(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
的特点创立LayoutParams
params
,当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
,不然会崩溃。