View绘制源码浅析(二)布局的测量、布局、绘制

本文详细分析了Android中View的绘制流程,包括测量(measure)、布局(layout)和绘制(draw)三个步骤。测量阶段,重点介绍了MeasureSpec的生成及View的测量过程;布局阶段,讲解了如何确定View的位置;绘制阶段,阐述了从到的绘制流程,以及如何通过onDraw方法完成内容的绘制。通过对这些核心流程的理解,有助于深入掌握Android视图的显示机制。
摘要由CSDN通过智能技术生成

前言

在第一篇View绘制源码浅析(一)布局的加载我们知道了setContentView()完成了DecorView的创建,并且将xml中的布局标签转换成了对应的View、属性转换成了对应的LayoutParams然后添加到了id为content的布局上,也就是说完成了布局对象的创建并且和DecorView关联上了。

那么第二篇将介绍View是如何显示出来的,主体流程可分为测量、布局、绘制这三步。

本文源码基于API27,接下来我们开始吧。

概述

绘制的开始是在Activity收到AMS的Resume事件,然后给DecorView设置上ViewRootImpl这个视图结构的顶部对象作为DecorViewparent,然后通过调用ViewRootImplrequestLayout()触发测量、布局、绘制的流程。

对于ViewRootImpl来说是一个包含了父布局功能的视图顶层对象(因为每个View都有parent属性指向父布局,而DecorView已经是最外层的布局了是没有父布局的,所以指向的是ViewRootImpl),不过需要注意它不是一个View。

Activity收到Resume事件后最终会走道ViewRootImplsetView()

    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
   
        ...
        requestLayout();//触发测量、布局、绘制
        res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                                          getHostVisibility(), mDisplay.getDisplayId(),
                                          mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                                          mAttachInfo.mOutsets, mInputChannel);//在window上显示
        ...
    }

requestLayout()最终会走到performTraversals()这个方法贼鸡儿长,我们只看重点

    private void performTraversals() {
   
        ...
        measureHierarchy(host, lp, res,
                         desiredWindowWidth, desiredWindowHeight);//会调到performMeasure(childWidthMeasureSpec, childHeightMeasureSpec)进行测量
        performLayout(lp, mWidth, mHeight);//布局
        performDraw();//绘制
        ...
    }

这里也就引出了我们的重点方法

  • performMeasure()测量
  • performLayout()布局
  • performDraw()绘制

接下来我们分别介绍这个过程。

测量

看测量前,我们得先了解一个概念measureSpec,它是一个32位的int值,是父布局的宽高约束和要测量的View的LayoutParams宽高共同作用生成的,作为测量方法的形参传入指导View的测量。其中高2位用于存储测量的模式,低30位用于存储具体的数值。然后为了方便生成和取出这个32位的int值,提供了一个工具类MeasureSpec

    public static class MeasureSpec {
   
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;//高两位的掩码
		//下面是3种模式
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;
        public static final int EXACTLY     = 1 << MODE_SHIFT;
        public static final int AT_MOST     = 2 << MODE_SHIFT;

      	//传入size和mode生成MeasureSpec
        public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                          @MeasureSpecMode int mode) {
   
            if (sUseBrokenMakeMeasureSpec) {
   
                return size + mode;
            } else {
   
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }

		//拿到mode
        @MeasureSpecMode
        public static int getMode(int measureSpec) {
   
            //noinspection ResourceType
            return (measureSpec & MODE_MASK);
        }

		//拿到size
        public static int getSize(int measureSpec) {
   
            return (measureSpec & ~MODE_MASK);
        }

    }

模式一共是有三种,这里先简单介绍下

  1. UNSPECIFIED
    未指定的,大小根据size来决定

  2. EXACTLY
    大小有明确的值,比如width为32或者macth_parent都适用该模式

  3. AT_MOST
    对应上wrap_content这种情况,size为View可用的最大值

通过makeMeasureSpec()可生成32位的measureSpecgetMode()getSize()可拿到mode和size。

准备工作做完了,接下来回到主线看测量方法measureHierarchy()

    private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
            final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
   
        int childWidthMeasureSpec;
        int childHeightMeasureSpec;
        boolean windowSizeMayChange = false;
		...
        if (!goodMeasure) {
   
                childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);//获取根布局的MeasureSpec
                childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);//获取根布局的MeasureSpec
                performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);//执行测量方法
                if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) 			{
   
                    windowSizeMayChange = true;
                }
        }    
		...

        return windowSizeMayChange;
    }

前面不是说measureSpec是根据父布局的宽高约束和要测量的View的LayoutParams宽高共同作用生成的。而这里稍有不同因为DecorView是最外层的布局了,没有父布局给他生成measureSpec参数所以用window去生成一个。

desiredWindowWidth和desiredWindowHeight是屏幕的宽高,lp.width和lp.height默认是match_parent,接下来看下getRootMeasureSpec()生成的啥。

    private static int getRootMeasureSpec(int windowSize, int rootDimension) {
   
        int measureSpec;
        switch (rootDimension) {
   

        case ViewGroup.LayoutParams.MATCH_PARENT://走到这个分支
            // Window can't resize. Force root view to be windowSize.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }

可以看到生成的measureSpec的size为屏幕的尺寸,mode为MeasureSpec.EXACTLY,。然后将measureSpec作为performMeasure()的形参传入。

    private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
   
        if (mView == null) {
   
            return;
        }
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
        try {
   
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);//这里mView为DecorView,即调用DecorView.measure
        } finally {
   
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }

这里mView为DecorView为了简便我们直接看FrameLayoutmeasure()方法,而FrameLayout并未重写也没法重写因为是final修饰的,所以最终走到View的measure()

    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
   
		
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值