LayoutInflater
1. 什么是LayoutInflater?
- LayoutInflater是一个用于将xml布局文件加载为View或者ViewGroup对象的工具,也被称为布局加载器。它主要用于加载布局
2. LayoutInflater的基本用法
-
代码如下:
```xml
<?xml version="1.0" encoding="utf-8"?>
<Button xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button">
</Button>
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 首先要得到LayoutInflater的实例,通过LayoutInflater.from()方法获得
//该方法接收一个Context参数
val layoutInflater = LayoutInflater.from(this)
// 调用inflate()方法来加载布局,该方法接收两个参数
// 1.要加载的布局id
// 2.是否给该布局的外部再嵌套一层父布局,不需要的话传入null
val buttonLayout = layoutInflater.inflate(R.layout.button_layout,null)
// addView()方法就是将这个布局添加到主布局中
main_layout.addView(buttonLayout)
}
}
3. 源码分析
- inflate() :该方法创建了根布局的实例
- 第三个参数attachToRoot:
- 如果root为null,attachToRoot将失去作用,设置任何值都无意义
- 如果root不为null,attachToRoot设为true,则会给加载的布局文件指定一个父布局,即为root
- 如果root不为null,attachToRoot设为false,则会将布局文件最外层的所有layout属性进行设置,当该View被添加到父View当中时,这些layout属性会自动生效
- 在不设置attachToRoot参数的情况下,如果root不为null,attachToRoot参数默认为true
- 第三个参数attachToRoot:
/**
* Inflate a new view hierarchy from the specified XML node. Throws
* {@link InflateException} if there is an error.
* <p>
* <em><strong>Important</strong></em> For performance
* reasons, view inflation relies heavily on pre-processing of XML files
* that is done at build time. Therefore, it is not currently possible to
* use LayoutInflater with an XmlPullParser over a plain XML file at runtime.
*
* @param parser XML dom node containing the description of the view
* hierarchy.
* @param root Optional view to be the parent of the generated hierarchy (if
* <em>attachToRoot</em> is true), or else simply an object that
* provides a set of LayoutParams values for root of the returned
* hierarchy (if <em>attachToRoot</em> is false.)
* @param attachToRoot Whether the inflated hierarchy should be attached to
* the root parameter? If false, root is only used to create the
* correct subclass of LayoutParams for the root view in the XML.
* @return The root View of the inflated hierarchy. If root was supplied and
* attachToRoot is true, this is root; otherwise it is the root of
* the inflated XML file.
*/
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
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 (DEBUG) {
System.out.println("**************************");
System.out.println("Creating root view: "
+ name);
System.out.println("**************************");
}
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 {
// Temp is the root view that was found in the xml
// createViewFromTag()方法将节点名和参数传入,在内部调用createView()方法通过反射的方式创建了View对象
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
if (DEBUG) {
System.out.println("Creating params from root: " +
root);
}
// 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);
}
}
if (DEBUG) {
System.out.println("-----> start inflating children");
}
// Inflate all children under temp against its context.
rInflateChildren(parser, temp, attrs, true);
if (DEBUG) {
System.out.println("-----> done inflating children");
}
// 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);
}
// Decide whether to return the root that was passed in or the
// top view found in xml.
if (root == null || !attachToRoot) {
result = temp;
}
}
} catch (XmlPullParserException e) {
final InflateException ie = new InflateException(e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (Exception e) {
final InflateException ie = new InflateException(
getParserStateDescription(inflaterContext, attrs)
+ ": " + e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} finally {
// Don't retain static reference on context.
mConstructorArgs[0] = lastContext;
mConstructorArgs[1] = null;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
return result;
}
}
- rInflate() : 该方法循环遍历根布局下的子元素
/**
* Recursive method used to inflate internal (non-root) children. This
* method calls through to {@link #rInflate} using the parent context as
* the inflation context.
* <strong>Note:</strong> Default visibility so the BridgeInflater can
* call it.
*/
final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
boolean finishInflate) throws XmlPullParserException, IOException {
rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}
/**
* Recursive method used to descend down the xml hierarchy and instantiate
* views, instantiate their children, and then call onFinishInflate().
* <p>
* <strong>Note:</strong> Default visibility so the BridgeInflater can
* override it.
*/
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
final int depth = parser.getDepth();
int type;
boolean pendingRequestFocus = false;
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
if (type != XmlPullParser.START_TAG) {
continue;
}
final String name = parser.getName();
if (TAG_REQUEST_FOCUS.equals(name)) {
pendingRequestFocus = true;
consumeChildElements(parser);
} else if (TAG_TAG.equals(name)) {
parseViewTag(parser, parent, attrs);
} else if (TAG_INCLUDE.equals(name)) {
if (parser.getDepth() == 0) {
throw new InflateException("<include /> cannot be the root element");
}
parseInclude(parser, context, parent, attrs);
} else if (TAG_MERGE.equals(name)) {
throw new InflateException("<merge /> must be the root element");
} else {
final View view = createViewFromTag(parent, name, context, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
// rInflateChildren()方法查找这个View下的子元素,然后添加到父布局中
rInflateChildren(parser, view, attrs, true);
viewGroup.addView(view, params);
}
}
if (pendingRequestFocus) {
parent.restoreDefaultFocus();
}
if (finishInflate) {
parent.onFinishInflate();
}
}
4. 关于layout_width和layout_height
- 这里无论怎么设置Button的layout_width和layout_height都不会生效,因为这两个值在这里完全失去了作用。其实这两个属性不是用于设置view的大小的,而是设置View在布局中的大小
- 也就是说,首先View必须存在与一个布局中,之后这两个属性才能够生效。而例子中的Button不在任何一个布局中,所以才叫做layout_width而不是width
- 如果要让这两个属性生效,那么最简单的方式就是在Button外嵌套一层布局
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:layout_width="300dp"
android:layout_height="80dp"
android:text="Button"/>
</RelativeLayout>
- 此时button存在于RelativeLayout中,两个属性也就生效了,但是最外层的RelativeLayout的两个属性又会失效
5. setContentView()
-
平时在Activity中指定布局文件的时候,最外层的布局是可以指定大小的,也就是说layout_width和layout_height是生效的。原因就是setContentView()方法
-
通过该方法,Android会自动在布局文件的最外层再嵌套一个FrameLayout,所以两个属性就可以生效了
-
通过getParent()方法,就能够获得FrameLayout:
Log.d("TestActivity"," "+main_layout.parent)
-
Activity显示的界面主要由标题栏和内容布局组成,内容布局就是一个FrameLayout,这个布局的id是content,当调用setContentView()方法时传入的布局就是放入这个FrameLayout中的,所以叫做setContentView()而不是setView()
-
组成图如下:
View的绘制流程
1. 什么是View?
- Android中的任何一个布局,任何一个控件都是直接或间接继承自View的。任何一个View都不会凭空出现在屏幕上,每一个的绘制过程都必须经历最主要的三个阶段: onMeasure() onLayout() onDraw()
2. onMeasure()
-
View的绘制流程会从ViewRoot的performTraversals()方法中开始,在其内部调用View的measure()方法。measure()方法接收两个参数,widthMeasureSpec和heightMeasureSpec分别用于确定视图的宽度和高度
-
MeasureSpec的值由specSize和specMode共同组成,specSize记录大小,specMode记录规格,一共有三种类型如下:
1.EXACTLY
表示父视图希望子视图的大小应该由specSize的值决定,系统默认会按照这个规则来设置子视图的大小
2.AT_MOST
表示子视图最多只能是specSize中指定的大小,开发人员应该尽可能小的去设置这个视图,并保证不会超过specSize
3.UNSPECIFIED
表示开发人员可以将视图按照自己的意愿设置成任意的大小,没有任何限制(使用较少) -
源码分析
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
* onMeasure()方法是真正去测量并设置View大小的地方,默认会调用getDefaultSize()方法来获取视图的大小
* measureSpec从measure()方法中传递过来,调用getMode和getSize可以解析出specMode和specSize。
* 如果specMode是AT_MOST或者EXACTLY就返回specSize,之后会在onMeasure()中调用setMeasuredDimension()方法来设定测量出的大小
* 一个界面的展示通常会涉及到多次measure,因为一个布局中一般会包含多个子视图,每个视图都需要经历一次measure过程,ViewGroup中定义了一个measureChildren()方法来测量子视图的大小。代码如下:
```java
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
3. onLayout()
-
measure过程结束后,视图的大小就已经测量好了,然后会通过onLayout()方法用于给视图进行布局,确定视图的位置。ViewRoot的performTraversals()方法会在measure结束后继续执行,并调用View的layout()方法来执行此过程:
host.layout(0, 0, host.mMeasuredWidth, host.mMeasuredHeight);
-
代码如下:
public void layout(int l, int t, int r, int b) {
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
// 首先调用setFrame()方法判断视图的大小是否发生变化,以确定有没有必要对当前的视图进行重绘,同时把传递进来的参数分别赋值,接下来会调用onLayout()方法
boolean changed = setFrame(l, t, r, b);
if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {
if (ViewDebug.TRACE_HIERARCHY) {
ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT);
}
// onLayout()方法是一个空方法,因为onLayout()过程是为了确定视图在布局中所在的位置,而这个操作是由布局来完成的,即父视图决定子视图的位置
// ViewGroup中的onLayout()方法是一个抽象方法,也就是说所有子类都必须重写这个方法
onLayout(changed, l, t, r, b);
mPrivateFlags &= ~LAYOUT_REQUIRED;
if (mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>) mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
mPrivateFlags &= ~FORCE_LAYOUT;
}
- getWidth()和getMeasureWidth()
- getMeasureWidth()在measure()方法结束后就能够获取到了,而getWidth()要在layout()过程结束后才能够获取到。
- getMeasureWidth()方法中的值是通过setMeasuredDimension()来进行设置的,而getWidth()方法中的值是通过视图右边的坐标减去左边的坐标计算出来的
4. onDraw()
- measure()和layout()过程都结束后,就会进入draw的过程,在这里会对视图进行真正地绘制。ViewRoot中的代码会继续执行并创建出一个Canvas对象,然后调用View的draw()方法来执行具体的绘制工作
- 代码如下:
public void draw(Canvas canvas) {
if (ViewDebug.TRACE_HIERARCHY) {
ViewDebug.trace(this, ViewDebug.HierarchyTraceType.DRAW);
}
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & DIRTY_MASK) == DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~DIRTY_MASK) | DRAWN;
// Step 1, draw the background, if needed
int saveCount;
if (!dirtyOpaque) {
// mBGDrawable对象就是在XML中通过android:background属性设置的图片或颜色,也可以在代码中通过setBackgroundColor(),setBackgroundResource()等方法进行赋值
final Drawable background = mBGDrawable;
if (background != null) {
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if (mBackgroundSizeChanged) {
background.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
mBackgroundSizeChanged = false;
}
if ((scrollX | scrollY) == 0) {
background.draw(canvas);
} else {
canvas.translate(scrollX, scrollY);
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
}
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
// 该方法也是一个空方法,因为每个视图的内容部分各不相同,所以这部分交给子类去实现
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
// 空方法,对当前视图的所有子视图进行绘制,ViewGroup的dispatchDraw()方法中就有具体的绘制代码
dispatchDraw(canvas);
// Step 6, draw decorations (scrollbars)
// 对视图的滚动条进行绘制,任何一个视图都是有滚动条的,知识一般情况下没有让它们显示出来
onDrawScrollBars(canvas);
// we're done...
return;
}
}
-
重写onDraw()方法 (绘制出来的图如下)
class MyView(context: Context,attrs:AttributeSet) : View(context,attrs) {
lateinit var mPaint : Paint
init {
mPaint = Paint(Paint.ANTI_ALIAS_FLAG)
}
override fun onDraw(canvas: Canvas?) {
mPaint.setColor(Color.YELLOW)
canvas?.drawRect(0F, 0F, width.toFloat(), height.toFloat(),mPaint)
mPaint.setColor(Color.BLUE)
mPaint.textSize = 20F
canvas?.drawText("Hello View",0F, (height/2).toFloat(),mPaint)
}
}
![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9mdXR1LmxleGlhbmdsYS5jb20vYXNzZXRzLzkzYjE2NjkwOWU3MDExZWE5OTE0MGE1OGFjMTNkNTQw?x-oss-process=image/format,png)