View的绘制流程

LayoutInflater

1. 什么是LayoutInflater?

  • LayoutInflater是一个用于将xml布局文件加载为View或者ViewGroup对象的工具,也被称为布局加载器。它主要用于加载布局

2. LayoutInflater的基本用法

  • 代码如下:

<?xml version="1.0" encoding="utf-8"?>



```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:
      1. 如果root为null,attachToRoot将失去作用,设置任何值都无意义
      2. 如果root不为null,attachToRoot设为true,则会给加载的布局文件指定一个父布局,即为root
      3. 如果root不为null,attachToRoot设为false,则会将布局文件最外层的所有layout属性进行设置,当该View被添加到父View当中时,这些layout属性会自动生效
      4. 在不设置attachToRoot参数的情况下,如果root不为null,attachToRoot参数默认为true
/**
    * Inflate a new view hierarchy from the specified XML node. Throws
    * {@link InflateException} if there is an error.
    * <p>
    * <em><strong>Important</strong></em>&nbsp;&nbsp;&nbsp;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()
    1. getMeasureWidth()在measure()方法结束后就能够获取到了,而getWidth()要在layout()过程结束后才能够获取到。
    2. 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)






  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值