转载请注明出处:http://blog.csdn.net/singwhatiwanna/article/details/38426471(来自singwhatiwanna的csdn博客)java
Android View系统解析系列:android
介绍View的基础知识、View的滑动、弹性滑动、滑动冲突解决方式、事件分发等github
Android View系统解析(下)
canvas
介绍View的Framework层原理、View的measure / layout / draw三大流程和一些高级技巧api
本次主要介绍下半部分,提纲以下网络
View的绘制过程oop
measure/layout/draw 工做流程
布局
识别 MeasureSpec 并可以 make 合适的 MeasureSpec
post
在渲染前获取 View 的宽高
构造特殊的 View
自定义View
自定义View分类
自定义 View 须知
ViewRoot
对应于 ViewRootImpl 类,是链接 WindowManager 和 DecorView 的纽带。
ActivityThread 中当 activity 对象被建立好后,会将 DecorView 加入到 Window中同时完成 ViewRootImpl 的建立并创建和 DecorView 的联系。
root = new ViewRootImpl(view.getContext(), display);
root.setView(view, wparams, panelParentView);
view 的绘制流程从 ViewRoot 的 performTraversals 开始,代码流程是这样的:
performMeasure -> measure -> onMeasure
performLayout -> layout -> onLayout
performDraw -> draw -> onDraw
由下图可知,DecorView做为顶级View,通常状况下它有上下两部分组成(具体状况会和api版本以及Theme有关),上面是title,下面是content,在activity中咱们调用setContentView所设置的view其实就是被加到content中,而如何获得content呢,能够这样:ViewGroup group = findViewById(R.android.id.content),如何获得咱们所设置的view呢,能够这样:group.getChildAt(0)。同时,经过源码咱们能够知道,DecorView实际上是一个FrameLayout。这里要说明的一点是View层的大部分事件都是从DecorView传递到咱们的view中的。
MeasureSpec
封装了父容器对 view 的布局上的限制,内部提供了宽高的信息( SpecMode 、 SpecSize ),SpecSize是指在某种SpecMode下的参考尺寸,其中SpecMode 有以下三种:
UNSPECIFIED
父容器不对 view 有任何限制,要多大给多大
EXACTLY
父容器已经检测出 view 所须要的大小
AT_MOST
父容器指定了一个大小, view 的大小不能大于这个值
MeasureSpecs 的意义
经过将 SpecMode 和 SpecSize 打包成一个 int 值能够避免过多的对象内存分配,为了方便操做,其提供了打包 / 解包方法
MeasureSpec
表明一个 32 位 int 值
高 2 位表明 SpecMode ,低 30 位表明 SpecSize
下面先看一下MeasureSpec 内部的一些常量的定义,经过下面的代码,应该不难理解MeasureSpec的工做原理
private static final int MODE_SHIFT = 30; private static final int MODE_MASK = 0x3 << MODE_SHIFT; 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; public static int makeMeasureSpec(int size, int mode) { if (sUseBrokenMakeMeasureSpec) { return size + mode; } else { return (size & ~MODE_MASK) | (mode & MODE_MASK); } } public static int getMode(int measureSpec) { return (measureSpec & MODE_MASK); } public static int getSize(int measureSpec) { return (measureSpec & ~MODE_MASK); }
这里分析下顶级容器DecorView的MeasureSpec的产生过程
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
上述代码描述了DecorView的MeasureSpec的产生过程,为了更清晰地了解,咱们继续看下去
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; }经过上述代码,顶级容器DecorView的MeasureSpec的产生过程就很明确了,具体来讲其遵照以下规则:
根据它的LayoutParams中的宽高的参数来分,
LayoutParams.MATCH_PARENT:其模式为精确模式,大小就是窗口的大小
LayoutParams.WRAP_CONTENT:其模式为最大模式,大小不定,可是不能超过窗口的大小
固定大小(好比100dp):其模式为精确模式,大小为LayoutParams中指定的大小
针对上表,这里再作一下具体的说明。前面已经提到,对于应用层 View ,其 MeasureSpec 由父容器的 MeasureSpec 和自
身的 LayoutParams 来共同决定,那么针对不一样的父容器和view自己不一样的LayoutParams,view就能够有多种MeasureSpec。这里简单说下,当view采用固定宽高的时候,无论父容器的MeasureSpec是什么,view的MeasureSpec都是精确模式而且其大小遵循Layoutparams中的大小;当view的宽高是match_parent时,这个时候若是父容器的模式是精准模式,那么view也是精准模式而且其大小是父容器的剩余空间,若是父容器是最大模式,那么view也是最大模式而且其大小不会超过父容器的剩余空间;当view的宽高是wrap_content时,无论父容器的模式是精准仍是最大化,view的模式老是最大化而且大小不能超过父容器的剩余空间。可能你们会发现,在咱们的分析中漏掉了Unspecified模式,这个模式主要用于系统内部屡次measure的状况下,通常来讲,咱们不须要关注此模式。
view#onMeasure 的默认实现
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(),
widthMeasureSpec),getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
注意:
经过 onDraw 派生的 View ,须要重写 onMeasure 并设置 wrap_content 时的自
身大小,不然使用 wrap_content 就至关于用 match_parent 。
缘由分析:见上面的表格
那么如何重写onMeasure从而让view支持wrap_content呢?请参看下面的典型代码,须要注意的是,代码中的mWidth和mHeight指的是view在wrap_content下的内部规格,而这一规格(宽高)应该由自定义view内部来指定。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(mWidth, mHeight); } else if (widthSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(mWidth, heightSpecSize); } else if (heightSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(widthSpecSize, mHeight); } }
这是一个比较有意义的问题,或者说有难度的问题,问题的背景为:有时候咱们须要在view渲染前去获取其宽高,典型的情形是,咱们想在onCreate、onStart、onResume中去获取view的宽高。若是你们尝试过,会发现,这个时候view尚未measure好,宽高都为0,那到底该怎么作才能正确获取其宽高呢,下面给出三种方法
Activity/View#onWindowFocusChanged :这个方法代表,view已经初始化完毕了,宽高已经准备好了
view.post(runnable) :经过post能够将一个runnable投递到消息队列的尾部,而后等待looper调用此runnable的时候,view也已经初始化好了
view.measure(int widthMeasureSpec, int heightMeasureSpec) :经过手动去measure来视图获得view的宽高
前两种方法都比较好理解也比较简单,这里主要介绍下第三种方法的详细用法:
采用 view.measure 去提早获取 view 的宽高,根据 view 的 layoutParams 来分
match_parent
直接放弃,没法 measure 出具体的宽高
具体的数值( dp/px )
好比宽高都是 100px ,以下 measure :
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec, heightMeasureSpec);
wrap_content
以下 measure :
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,也就是(1 << 30) - 1,在最大化模式下,咱们用view理论上能支持的最大值去构造MeasureSpec是合理的。
关于view的measure,网络上有两个错误的用法,以下,为何说是错误的,首先违背了系统的内部实现规范(由于没法经过错误的MeasureSpec去得出合法的SpecMode从而致使measure出错),其次不能保证必定能 measure 出正确的结果。
第一种错误用法
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(-1, MeasureSpec.UNSPECIFIED);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(-1, MeasureSpec.UNSPECIFIED);
view.measure(widthMeasureSpec, heightMeasureSpec);
第二种错误用法
view.measure(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
layout 的主要做用
ViewGroup 用来肯定子元素的位置。
流程
当 viewgroup 的位置被肯定后,它在 onLayout 会遍历全部的 child 并调用其 layout 。在 layout 中 onLayout 会被调用。
关键方法
public void layout(int l, int t, int r, int b)
onLayout(changed, l, t, r, b)
问题:如何让 getWidth 和 getMeasuredWidth 返回的值不同?
private void setChildFrame(View child, int left, int top, int measuredWidth, int measureHeight) {
child.layout(left, top, left + measuredWidth, top + measureHeight);
}
int width = right - left;
int height = bottom - top
方法
在父容器的 onLayout 中经过 child.layout 来放置 view 到任意位置
在本身的 onLayout 中修改 mLeft/mRight/mTop/mBottom
draw 的大体流程
a. 画背景 background.draw(canvas)
b. 绘制本身( onDraw )
c. 绘制 children ( dispatchDraw )
d. 绘制装饰( onDrawScrollBars )
备注:
dispatchDraw 会遍历调用全部 child 的 draw ,如此 draw 事件就一层层地传递了下去
继承 View 重写 onDraw
继承 ViewGroup 派生特定的 Layout
继承特定的 View (好比 TextView , ListView )
继承特定的 Layout (好比 LinearLayout )