博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
《Android艺术开发探索》学习笔记之View的工作原理
阅读量:7087 次
发布时间:2019-06-28

本文共 11361 字,大约阅读时间需要 37 分钟。

初识ViewRoot和DecorView

ViewRoot对于ViewRootImpl类,它是连接WindowManager和DecorView的纽带,View的大三流程均是通过ViewRoot来完成的。在ActivityThread中,当Activity对象被创建完毕后,会将De'corView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和De'corView建立关联。 过程源码:

root = new ViewRootImpl(view,getContext(),display);root.addView(view,wparams,panelParentView);复制代码

View的绘制流程是从RootView的performTraverslas方法开始的,它经过measure,layout和draw三个过程才能最终将一个View绘制出来。 measure:测量View的宽和高 layout:确定View在父容器中的放置位置 draw:负责将View绘制在屏幕上

针对performTraversals的大致流程,如下图所示:

performTraversals依次调用preformMeasure、preformLayout和performDraw三个方法,这个三个方法分别完成顶级View的measure、layout和draw。其中preformMeasure中会调用measure方法,在measure方法中又会调用onMeasure方法,在onMeasure方法中则会对所有的子元素进行measure过程,这个时候measure流程就从父容器传递到子元素中了,这样就完成了一次measure过程。接着子元素会重复父容器的measure过程,如此反复就完成了整个View树的遍历。同理,preformLayout和performDraw的传递流程和preformMeasure是类似的,唯一不同的是,preformDraw的传递过程是在draw方法中通过dispatchDraw来实现的,不过并没有本质区别。

measure过程决定了View的宽和高,measure完成以后,可以通过getMeasureWidth和getMeasureHeight方法获得View测量后的宽/高,在几乎所有的情况下它都等同于View最终的宽/高。

layout过程决定了View的四个顶点的坐标和实际VIew的宽/高,完成以后,可以通过getTop、getBottom、getLeft和getRight来拿到View的四个顶点的位置。并可以通过getWidth和getHeight方法拿到View的最终宽/高。

Draw过程决定了View的显示,只有通过draw方法完成以后View的内容才能呈现在屏幕上。

DecorView作为顶级View,一般情况下它内部会包含一个竖直方向的LinearLayout,在这个LinearLayout里面又上下两个部分(具体情况和Android版本以及主题有关),上面是标题栏,下面是内容栏,在Activity中我们通过setContentView所设置的布局其实就是被加到内容栏中的,而内容栏的id是content,因此可以理解为Activity指定的布局方法不叫setView而叫setContentView,因为我们的布局的确加到了id为content的FrameLayout中。 获取content:ViewGroup content = findViewById(R.android.content)。 获取我们设置的VIew:content.getChildAt(0). 从源码中获知,DecorView其实是一个FrameLayout,View层的事件都先经过DecorView,然后才传递给我们的View。

理解MeasureSpec

MeasureSpec很大程度上决定 了一个View的尺寸规格,之所以说是很大程度上是因为这个过程还受父容器影响,因为父容器影响View的MeasureSpec的创建过程。

在测量过程中,系统会将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,然后根据这个MeasureSpec来测量出View的宽/高。

MeasureSpec

MeasureSpec代表一个32位int值,高2位代表SpecMode,低30位代表SpecSize。 SpecMode:测量模式 SpecSize:某种测量模式下的规格大小

SpecMode有三类: 1)UNSPECIFIED:父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量的状态。

EXACTLY:父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值。它对应于LayoutParams中的match_parent和具体的数值这两种模式。

AT_MOST:父容器指定一个可用大小,即SpecSize,View的大小不能超过这个值,具体是什么值要看不同的View的具体实现。它对应于LayoutParams中的wrap_content。

MeasureSpec和LayoutParams的对应关系

MeasureSpec不是唯一由LayoutParams决定的,LayoutParams需要和父容器一起才能决定View的MeasureSpec,从而进一步决定View的宽/高。另外,对于顶级View(即DecorView)和普通View来说,MeasureSpec的转换过程略有不同。

DecorView:其MeasureSpec由窗口的尺寸和自身的LayoutParams来共同决定。DecorView的MeasureSpec产生过程,具体遵守如下规则,根据它的LayoutParams中的宽/高参数来划分。 1)LayoutParams.MATCH_PARENT:精确模式,大小就是窗口的大小;

2)LayoutParams.WRAP_CONTENT:最大模式,大小不定,但是不能超过窗口大小;

3)固定大小(比如100dp):精确模式,大小为LayoutParams中指定的大小。

普通View:其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams来共同决定,MeasureSpec一旦确定后,onMeasure中就可以确定View的测量宽/高。其创建规则如下图所示

当View采用固定宽/高的时候,不管父容器的MeasureSpec是什么,View的MeasureSpec都是精确模式并且其大小遵循LayoutParams中的大小。当View的宽/高是match_parent时,如果父容器的模式是精确模式,那么View也是精确模式并且大小是父容器的剩余空间;如果父容器是最大模式,那么View也是最大模式并且大小不会超过父容器的剩余空间。当View的宽/高是wrap_content时,不管父容器的模式是精准还是最大化,View的模式总是最大化并且不超过父容器的剩余空间。

UPSPECIFIE模式:主要作用于系统内部多次measure的情形,一般来说,我们不需要关注此模式。

View的工作流程

View的工作流程主要是指measure、layout、draw这三大流程,即测量、布局和绘制,其中measure确定View的测量宽/高,layout确定View的最终宽/高和四个顶点位置,而draw将View绘制到屏幕上。

measure过程

原始的View:通过measure方法就完成了其测量过程; ViewGroup:除了完成自己的测量过程外,还会遍历去调用所有子元素的measure方法,各个子元素再递归去执行这个流程。

1、View的Measure过程 View的measure过程由其measure方法来完成,measure是一个final类型的方法,这意味着子类不能重写此方法,在View的measure方法中会去调用View的onMeasure方法,因此只需要看onMeasure的实现即可。

View的onMeasure方法如下:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));    }复制代码

setMeasuredDimension方法会这是View的宽/高的测量值

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;    }复制代码

getDefaultSize返回的大小就是measureSpec中的SpecSize,而这个spectSize就是View测量后的大小,是因为View的最终大小是在layout阶段确定的,所以这里必须加以区分,但是几乎所有情况下View的测量大小和最终大小是相等的。

至于UNSPECIFIED这种情况,一般用于系统内部的测量过程,在这种情况下,View的大小为getDefaultSize的第一个参数size,即宽/高分别为getSuggestedMinimumWidth和getSuggestedMinimumHeight这两个方法的返回值。

protected int getSuggestedMinimumWidth() {        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());    }    protected int getSuggestedMinimumHeight() {        return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());    }复制代码

getSuggestedMinimumWidth和getSuggestedMinimumHeight实现原理一样,getSuggestedMinimumWidth分析:如果View没有设置背景,那么View的宽度为mMinWidth,而mMinWidth对应于Android:minWidth这个属性所指定的值,因此View的宽度即为Android:minWidth属性所指定的值。这个属性如果不指定,那么mMinWidth则默认为0;如果View指定了背景,则View的宽度为max(mMinWidth,mBackground.getMinimumWidth()),即Android:minWidth和背景的最小宽度这两者中的最大值。

从getDefaultSize方法的实现来看,View的宽/高是由spectSize决定,所以我们可以得出:直接继承View的自定义控件需要重写onMeasure方法并设置wrap_content时的自身大小,否则在布局中使用wrap_content就相当于使用match_parent。造成的原因:上述代码和普通View的MeasureSpec规则表中分析得出,如果View在布局中使用wrap_content,那么它的specMode是AT_MOST模式,在这种模式下,它的宽/高等于specSize,在这模式下,它的宽/高等于specSize,从表中可知,这种情况下View的specSize是parentSize,而parentSize是父容器中目前可以使用的大小,也就是父容器当前剩余的空间大小,很显然,View的宽/高就等于父容器当前剩余空间大小,这种效果和在布局中使用match_parent完全一致。 解决办法:

@Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        super.onMeasure(widthMeasureSpec, heightMeasureSpec);        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);        if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){            setMeasuredDimension(200,200);        }else if (widthSpecMode == MeasureSpec.AT_MOST){            setMeasuredDimension(200,heightSpecSize);        }else if (heightSpecMode == MeasureSpec.AT_MOST){            setMeasuredDimension(widthSpecSize,200);        }    }复制代码

从上面代码中得知,只需要给View指定一个默认的内部宽/高,并在wrap_content时设置宽/高即可。对于非wrap_content情形,我们沿用系统的测量值即可,至于默认的内部宽/高的大小如何指定,这个没有固定的依据,根据需要灵活指定即可,如果查看TextView、ImageView等源码就可以知道,针对wrap_content情形,他们的onMeasure方法均做了特殊处理。

ViewGroup的onMeasure过程

对于ViewGroup来说,除了完成自己的measure之外,还会遍历去调用子元素的measure方法,各个子元素再去递归去执行这个过程。和View不同的是,ViewGroup是一个抽象类,因此它没有重写View的onMeasure方法,但是它提供了一个叫measureChildren的方法。

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);    }复制代码

measureChild的思想就是取出子元素的LayoutParams,然后再通过getChildMeasureSpec来创建子元素的MeasureSpec,接着将MeasureSpec直接传递给View的measure方法来进行测量。

View的measure过程是三大流程中最复杂的一个,measure完成以后,通过getMeasureWidth/height方法就可以正确地获取到View的测量宽/高。需要注意的是,在某些极端情况下,系统可能需要多次measure才能正确最终的测量宽/高,在这种情形下,在onMeasure方法中拿到的测量宽/高可能是不准确的。一个比较好的习惯是在onLayout方法中去获取View的测量宽/高或者最终宽/高。

Activity启动时获取某个View的宽/高:由于onCreate、onStart、onResume中均无法正确得到某个View的宽/高信息,这是因为View的measure过程和Activity的生命周期方法不是同步执行的,因此无法保证Activity执行了onCreate、onStart、onResume时某个View已经测量完毕了,如果View还没有测量完毕,那么获取的宽/高就是0。解决方法:

1)Activity/View#onWindowFocusChanged onWindowFocusChanged这个方法的含义是:View已经初始化完毕了,宽/高已经准备好了,这个时候去获取宽/高是没问题的。需要注意的是,onWindowFocusChanged会被调用很多次,当Activity的窗口得到焦点和失去焦点时均被调用一次。具体来说,当Activity继续执行和暂停执行时,onWindowFocusChanged均会被调用,如果频繁地进行onResume和onPause,那么onWindowFocusChanged也会频繁地调用。 使用onWindowFocusChanged的典型代码:

@Override	public void onWindowFocusChanged(boolean hasWindowFocus) {		super.onWindowFocusChanged(hasWindowFocus);		if (hasWindowFocus){			int width = view.getMeasuredWidth();			int height = view.getMeasuredHeight();		}	}复制代码

2)view.post(runnable) 通过post 可以讲一个runnable投递到消息队列的尾部,然后等待Looper调用此runnable的时候,View也已经初始化好了。 代码如下:

@Override	protected void onStart() {		super.onStart();		view.post(new Runnable() {			@Override			public void run() {				int width = view.getMeasuredWidth();				int height = view.getMeasuredHeight();			}		});	}复制代码

3)ViewTreeObserver 当View树的状态发生改变或者View树内部的View的可见性发生改变时,onGlobalLayout方法将被回调,因此这是获取View的宽/高一个很好的时机,需要注意的是,伴随着View树的状态改变等,onGlobalLayout会被调用多次。 典型代码如下:

@Override	protected void onStart() {		super.onStart();		ViewTreeObserver observer = view.getViewTreeObserver();		observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {			@Override			public void onGlobalLayout() {				view.getViewTreeObserver().removeGlobalOnLayoutListener(this);				int width = mListView.getMeasuredWidth();				int height = mListView.getMeasuredHeight();			}		});	}复制代码

4)view.measure(int widthMeasureSpec,int heightMeasureSpec) 通过手动对View进行measure来得到View的宽/高。这种情况比较复杂,这里要分情况处理,根据View的LayoutParams来分:

match_parent:直接放弃,无法measure出具体的宽/高。 具体的数值(dp/px):

int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);view.measure(widthMeasureSpec ,heightMeasureSpec );复制代码

wrap_content:

int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1 << 30)-1,MeasureSpec.AT_MOST);int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1 << 30)-1,MeasureSpec.AT_MOST);view.measure(widthMeasureSpec ,heightMeasureSpec );复制代码

(1 << 30)-1,通过分析MeasureSpec的实现可以知道,View的尺寸使用30位二进制表示,也就是说最大是30个1(即2^30)-1,在最大模式下,我们使用view理论上能支持的最大值去构造MeasureSpec是合理的。

错误用法: 第一种错误用法:

int widthMeasureSpec = MeasureSpec.makeMeasureSpec(-1,MeasureSpec.UNSPECIFIE);int heightMeasureSpec = MeasureSpec.makeMeasureSpec((-1,MeasureSpec.UNSPECIFIE);view.measure(widthMeasureSpec ,heightMeasureSpec );复制代码

第二种错误用法: view.measure(LayoutParams.WRAP_CONTENT,LayoutParams.WRAP_CONTENT);

layout过程

layout的作用是ViewGroup用来确定子元素的位置,当ViewGroup的位置确定后,它在onLayout中会遍历所有的子元素并调用其layout方法,在layout方法中onLayout方法又会被调用。layout方法确定View本身的位置,而onLayout方法则会确定所有子元素的位置。

layout方法的大致流程:首先通过setFrame方法来设定view的四个顶点位置,即初始化mLeft、mTop、mRight和mBottom这四个值,View的顶点一旦确定,那么View的父容器中的位置也就确定了;接着会调用onLayout方法,这个方法的用途是父容器确定子元素的位置,和onMeasure方法类似,onLayout的具体实现同样和具体的布局有关,所以View和ViewGroup均没有真正实现onLayout方法。

draw过程

View的绘制过程遵循如下几步:

1)绘制背景background.draw(canvas)

2)绘制自己

3)绘制children(dispatchDraw)

4)绘制装饰(onDrawScrollBars)

View的绘制过程的传递是通过dispatchDraw来实现的,dispatchDraw会遍历调用所有子元素的draw方法,如此draw事件就一层层地传递下去。View有一个特殊的方法setWillNotDraw:如果一个View不需要绘制任何内容,那么设置这个标志位为true后,系统会进行相应的优化。默认情况下,View没有启用这个优化标志位,但是ViewGroup会默认启用这个优化标志位。这个标志位对实际开发的意义是:当我们自定义控件继承于ViewGroup并且本身不具备绘制功能时,就可以开启这个标志位从而便于系统进行后续的优化。当然,当明确知道一个ViewGroup需要通过onDraw来绘制内容时,我们需要显式地关闭WILL_NOT_DRAW这个标志位。

转载地址:http://zggml.baihongyu.com/

你可能感兴趣的文章
Powershell管理系列(十二)Exchange新启用的邮箱禁用OWA及Activesync的访问
查看>>
Windows 8上安装本地回环网卡
查看>>
Exchange Server 2013系列十二:邮箱的基本管理
查看>>
[C#进阶系列]专题二:你知道Dictionary查找速度为什么快吗?
查看>>
并发连接数、请求数、并发用户数
查看>>
SDA报告给各国网络空间安全防卫水平进行评级
查看>>
去小机化思维(二)--【软件和信息服务】2015.03
查看>>
【翻译】Sencha Cmd中脚本压缩方法之比较
查看>>
最新.NET 5.0 C#6 MVC6 WCF5 NoSQL Azure开发120课视频
查看>>
爱因斯坦计划最新进展(201710)
查看>>
传统HA系统的终结者-【软件和信息服务】2013.11
查看>>
Spread for Windows Forms快速入门(15)---使用 Spread 设计器
查看>>
自动抓屏工具 -- psr
查看>>
jqPlot
查看>>
将Access换成sql要改些什么?注意哪些问题?(汇总)
查看>>
SQL中的union和union all区别(转)
查看>>
[转载]Dotnet程序集自动生成版本号
查看>>
电脑通过vnc控制android 手机
查看>>
Xml匹配为对象集合(两种不同的方式)
查看>>
用javascript实现网站来回撞动的广告图片
查看>>