您的位置:首页 > 其它

自定义View学习笔记03—View的工作原理简介

2017-12-25 16:55 323 查看
这篇博客有点长,但我想讲得还是比较清晰的,希望能坚持看完。

一、几个重要的概念:

1、MeasureSpec概述:

作用上简单地说就是测量View的Width/Height尺寸。一个子View的Width/Height尺寸同事受自身尺寸参数LayoutParams和父View尺寸的影响。测量过程中系统会将View的LayoutParams根据父View的MeasureSpec参数情况转换成自身的MeasureSpec,然后再根据自身的MeasureSpec测量Width/Height。


注意:这里测出来的Width/Height并不一定是最终的宽高,后面会详细解释原因。

2、MeasureSpec含义:

它是一个31位的int值,高2位为SpecMode,这个参数在我们自定义View的时候经常会用到,它代表测量模式。低30位代表SpecSize,代表SpecMode某种取值下测得的规格大小,具体如下:




在测量的时候,子View的LayoutParams和父容器的MeasureSpec一起决定子View的MeasureSpec,然后再根据MeasureSpec通过onMeasure确定子View的宽高。顶级View(DecorView)(根View)与此略有不同,但不做重点分析。

3、几个参数:

int SpecMode = MeasureSpec.getMode(spec); int SpecSize = MeasueSpec.getSize(spec);

int size = SpecSize – padding。

即:子View的尺寸 = 父容器的尺寸 - padding。子View的MeasureSpec创建规则如下:



表格说明:前面已经说过,子View的MeasueSpec是由父容器的MeasueSpec和自身的LayoutParams共同决定的,因此针对父容器的MeasueSpe不同和自身的LayoutParams不同,子View的MeasueSpec有多种不同的组合:



至于UNSPECIFED模式,主要用于系统级别的多次Measue,我们不需要关注这个。

二、View的工作流程

主要是指View的measure测量View的宽/高、layout确定View的最终宽/高和四个顶点的位置、draw将View绘制到屏幕上去这三大流程。这里我们先讲measure流程,layout流程和draw流程这两个我们后面讲。

measure():

完成View的测量,他是一个final类型的方法,这表示它不能被复写和重写。他调用onMeasure()方法完成View的测量:

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

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:
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}


简单的理解:getDefaultSize()返回的大小就是specSize,因为UNSPECIFIED模式我们通常用不上,AT_MOST模式下什么也没有做,EXACTLY模式下result=specSize。而这个specSize就是View测量后的大小,但不一定是最终的大小(虽然大概率是最终大小,但小部分情况下不是)。

再来看看getSuggestedMinimumWidth()和getSuggestedMinimumHeight()的源码:

protected int getSuggestedMinimumWidth() {
return mBackground==null ? mMinWidth:max(mMinWidth, mBackground.getMinimumWidth());
}

protected int getSuggestedMinimumHeight() {
return mBackground==null?mMinHeight:max(mMinHeight,mBackground.getMinimumHeight());
}

public int getMinimumWidth() {
final int intrinsicWidth = getIntrinsicWidth();
return intrinsicWidth > 0 ? intrinsicWidth : 0;
}

public int getMinimumHeight() {
final int intrinsicHeight = getIntrinsicHeight();
return intrinsicHeight > 0 ? intrinsicHeight : 0;
}


getSuggestedMinimumWidth/Height中的mMinWidth/mMinHeight由android:minWidth/minHeight在xml中设置,如果没有设置这个参数,则默认为0.

getMinimumWidth/Height返回的就是背景中Drawable的原始宽高,如果Drawable的原始宽高大于0,则返回具体的值,否则返回0.

结论:直接继承View的自定义控件需要重写OnMeasure方法并设置wrap_content时的自身大小,否则在布局中使用wrap_content的时候相当于使用match_parent。因为如果View在布局中使用wrap_content,他的SpecMode是AT_MOST模式,此时他的宽高就是SepcSize。查前面的表可知,在AT_MOST模式下,SepcSize的大小就是parentSize。亦即父类的SpecSize-padding。这就相当于使用match_parent。解决方法:重写onMeasure方法。具体如下:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
if(widthSpecMode == MeasureSpec.AT_MOST &&
heightSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(width, height);
}else if(widthSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(width, heightMeasureSpec);
}else if(heightSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(widthMeasureSpec, height);
}
}


针对自定义View,我们只需要给View制定一个默认的内部宽高(文中的width,height),并在wrap_content时设置该参数即可,对于非wrap_content情况,我们沿用系统的测量值即可,亦即:android:layout_width和android:layout_height两个属性,哪一个被设置为wrap_content,我们就在自定义的代码中使用对应的width/height。至于具体怎么指控,则没有固定的套路和规则。

三、ViewGroup的工作流程:

ViewGroup的工作流程与View稍有不同,它除了完成对自己的measure以外,还会去对自己所有的子View进行measure。这里我们先讲measure流程,layout流程和draw流程这两个我们后面讲。另外,ViewGroup是一个抽象类,他没有重写View的OnMeasure方法,而是额外提供了一个measureChildren的方法专门用来测量子View:

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);
}
}
}
private 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里面,去除子View的LayoutParams,然后getChildMeasureSpec方法创建子View的measureSpec并将其传给child的measure方法进行测量getChildMeasureSpec与前面的getSuggestedMinimumHeight/getSuggestedMinimumWidth方法原理相同,再此不累述。就这样,ViewGroup通过for循环完成对其自身子View的measure工作。

事实上ViewGroup并没有具体定义其测量过程,测量过程的onMeasure方法由继承他的子View(LinearLayout、RelativeLayout等)去具体实现,因为不同的子View有不同的布局特征,测量过程完全不同,无法给出统一的模式。

四、LinearLayout的Measure过程分析

LinearLayout的Measure方法很简单:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}


对于LinearLayout布局,只有两种方式要么是vertical竖直方向,要么是horizontal水平方向,因此就处理两个方向上的测量。

对于measureVertical和measureHorizontal方法都很长,几百行,在此不做展示。核心思路是:在两个方法里面会遍历所有的子View并对每一个子View执行measureChildBeforeLayout方法:

measureChildBeforeLayout(child,i,widthMeasureSpec,0,heightMeasureSpec,usedHeight);//LinearLayout.class的806行

void measureChildBeforeLayout(View child, int childIndex,int widthMeasureSpec, int totalWidth,int heightMeasureSpec,int totalHeight) {

measureChildWithMargins(child, widthMeasureSpec, totalWidth,heightMeasureSpec, totalHeight);
}//LinearLayout.class的1511/1514行


measureChildBeforeLayout方法里面会调用父类ViewGroup的measureChildWithMargins方法,获取子View的childWidthMeasureSpec和childHeightMeasureSpec参数,然后在调用父类View的measure方法进行测量,并调用onMeasure方法,至此又回到我们最开始的分析:

private void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp=(MarginLayoutParams)child.getLayoutParams();
final int childWidthMeasureSpec=getChildMeasureSpec(parentWidthMeasureSpec,mPaddingLeft+mPaddingRight+lp.leftMargin+lp.rightMargin+widthUsed,lp.width);
final int
childHeightMeasureSpec=getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop+mPaddingBottom+lp.topMargin+lp.bottomMargin+heightUsed,lp.height);
//child.measure在父类View/ViewGroup里,child是View的对象:View child=new View();
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}


measureVertical方法里面有一个用于记录累计高度/宽度的参数:mTotalLength,每测量一个子View的高度,就对它执行一次加法,每次增加的高度包括子View的高度及其marginTop/Bottom以及父容器的paddingTop/Bottom。完成对子View的测量后,View还要对自身进行测量。

最终测量完成后,通过getMeasuredWdith/Height方法拿到View的测量宽高。但某些极端情况下,View的measure测量要反复执行数次才能最终确定,因此我们自定义的时候最好是在OnLayout方法里面去获取View的宽高。

RelativeLayout的测量过程与此类似,但更复杂,在此不累述。

五、获取View宽高的几种方法

A、在Activity中复写onWindowFocusChanged方法并获取宽高:

public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if(hasFocus){
int width = mIvBitMap.getMeasuredWidth();
int height = mIvBitMap.getMeasuredHeight();
}
}


B、调用View.post/postDelayed方法。后者线程安全。

mIvBitMap.postDelayed(new Runnable() {
@Override
public void run() {
int widthPost = mIvBitMap.getMeasuredWidth();
int heightPost = mIvBitMap.getMeasuredHeight();
}
}, 1000);


C、使用ViewTreeObserver的回调接口OnGlobalLayoutListener接口就是其中之一。

D、通过view.measure方法手动测量,这种方法很复杂。

以上四种方法,最常用的是A和B两种方法,尤其是方法B。C、D两种方法几乎不用,在此不多说。

六、layout过程:

layout的作用是用来确定子View在ViewGroup中的位置。当ViewGroup的位置确定后,ViewGroup会调用OnLayout方法遍历他的所有子View,并调用其layout方法(layout方法里面调用了OnLayout方法)。layout方法确定View自身的位置,onLayout方法会确定所有子View的位置。ViewGroup的layout方法调用的是父类View的,View的layout方法具体实现如下:

public void layout(int l, int t, int r, int b) {
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
int oldL = mLeft;
int oldB = mBottom;
int oldR = mRight;
boolean changed=isLayoutModeOptical(mParent)?setOpticalFrame(l, t, r, b):setFrame(l, t, r, b);
if(changed||(mPrivateFlags&
PFLAG_LAYOUT_REQUIRED)==PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
if (shouldDrawRoundScrollbar()) {
if(mRoundScrollbarRenderer == null) {
mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
}
} else {
mRoundScrollbarRenderer = null;
}
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =           (ArrayList<OnLayoutChangeListener>)li.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 &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
notifyEnterOrExitForAutoFillIfNeeded(true);
}
}


大致流程是:

1、通过setFrame方法设定view的四个顶点的位置,即初始化mLeft、mRight、mTop、mBottom,确定了这四个值,就确定了View在父容器中的位置。

2、调用onLayout方法让父容器确定子View的位置。

值得注意的是:

1、ViewGroup的onLayout方法是抽象的:

private abstract void onLayout(boolean changed, int l, int t, int r, int b);


由继承他的子View(LinearLayout/RelativeLayout/FramLayout等)具体实现;

2、View的onLayout方法是空的,什么也没做:

private void onLayout(boolean changed,int left,int top,int right,int bottom){

}


同样由继承他的子View如TextView,ImageView等具体实现。

我们常用的LinearLayout的OnLayout方法如下:

private void onLayout(boolean changed,int l,int t,int r,int b){
if (mOrientation == VERTICAL) {
layoutVertical(l, t, r, b);
} else {
layoutHorizontal(l, t, r, b);
}
}


很明显的,OnLayout依照Vertical和Horizontal两种布局方式分别布局。这里仍以layoutVertical方法作分析:

void layoutVertical(int left, int top, int right, int bottom) {
………
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null) {
childTop += measureNullChild(i);
}
if (child.getVisibility() != GONE) {
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
final LinearLayout.LayoutParams lp =
(LinearLayout.LayoutParams) child.getLayoutParams();
int gravity = lp.gravity;
if (gravity < 0) {  gravity = minorGravity; }
final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(
gravity,layoutDirection);
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.CENTER_HORIZONTAL:
childLeft=paddingLeft+((childSpace-childWidth)/2)+
lp.leftMargin-lp.rightMargin;
break;
case Gravity.RIGHT:
childLeft=childRight-childWidth-lp.rightMargin;
break;
case Gravity.LEFT:
default:
childLeft = paddingLeft + lp.leftMargin;
break;
}
if (hasDividerBeforeChildAt(i)) {
childTop += mDividerHeight;
}
childTop += lp.topMargin;
setChildFrame(child,childLeft,childTop+getLocationOffset(child),
childWid,childHei);
childTop += childHeight + lp.bottomMargin +
getNextLocationOffset(child);
i += getChildrenSkipCount(child, i);

}
}
}

private void setChildFrame(View child,int left,int top,int width,int height){
// child是View.class的对象
child.layout(left, top, left + width, top + height);
}


layoutVertical方法会通过for循环遍历childView,并调用setChildFrame方法给对应的子View指定具体的位置,从setChildFrame的源码及参数来看,指定子View的位置时并不是直接指定View的四个顶点,而是指定ChildLife和childTop两个顶点,然后分别加上测得子View的宽高,得到另外两个顶点。每调用一次setChildFrame方法,childTop就会增大,这使得后面循环到的子View会被放在垂直方向靠后的位置。这正好符合了LinearLayout布局的Vertical属性的定义。而setChildFrame方法内部则是通过第一个参数child调用子View的layout方法,而该方法依然是父类View.class的,layout方法前面已经有分析,在此不累述。如此循环,直至完成整个ViewTree的layout过程。

疑问:View的测量宽高和最终宽高的区别——>>>>>

这个问题可以更具体的表述为——>>View的getMeasuredWidth/Height方法与View的getWidth/Height方法的区别:

我们先分别看他们的实现方式:

public final int getWidth() {
return mRight - mLeft;
}
public final int getHeight() {
return mBottom - mTop;
}


mLeft 、mRight、mBottom、mTop四个参数在View.class的setFrame方法中被赋值,而setFrame方法被layout方法和setOpticalFrame方法同时调用,传入setFrame方法的参数正是left,right,top,bottom。而layout方法被子类LinearLayout的setChildFrame(View child, int l, int t, int w, int h)方法以child.layout(l,t,r,b)的形式调用,其中child是View的对象,然后layoutHorizontal方法和layoutVertical方法调用setChildFrame方法,最终这两个方法被onLayout方法调用。onLayout方法复写自父类View.class。

再细说说参数l和childWidth的由来:

l = childLeft + getLocationOffset(child),
//若Drawable对象为空mDividerWidth=0,否则
//mDividerWidth = Drawable.getIntrinsicWidth()
childLeft += mDividerWidth。
childWidth = child.getMeasuredWidth()。


方法child.getMeasuredWidth来自于父类View.class。我们可以看看这个方法的实现:

public final int getMeasuredWidth() {
return mMeasuredWidth & MEASURED_SIZE_MASK;
}


其中mMeasuredWidth在View.class的setMeasuredDimensionRaw方法中被赋值:

private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
}


该方法被setMeasuredDimension(int measuredWidth, int measuredHeight)方法调用,而onMeasure (int widthMeasureSpec, int heightMeasureSpec)方法又调用了setMeasuredDimension方法,而widthMeasureSpec和heightMeasureSpec两个参数恰好是measure测量方法运行之后获得的包含具体的宽高尺寸和测量模式的数据,至此我们又回到了前面“二、View的工作流程”小节,并清晰地解释了getWidth方法中参数mRight和mLeft的来历,同时讲解了getMeasuredWidth()方法的调用。参数mBottom和mTop与此类似,不累述。

简单的说就是:getMeasuredWidth/Height获得的是measure测量方法运行过后的宽高。getWidth/Height方法获得的是layout布局方法运行过程中的宽高。二者的获得只是在时间上有些不同,但我们通常可以认为二者是相等的。极少数情况下二者有出入。尤其是在layout过程中对l,t,r,b四个参数做了处理的时候。

七、draw的过程

Draw的过程比前两个都要简单,它主要是将布局视图绘制到屏幕上,遵循以下几个步骤:

1. Draw the background
//Step 1, draw the background, if needed
if (!dirtyOpaque) {
drawBackground(canvas);
}
2. If necessary, save the canvas' layers to prepare for fading
// skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
3. Draw view's content
// Step 3, draw the content
if (!dirtyOpaque) {
onDraw(canvas);
}
4. Draw children
// Step 4, draw the children
dispatchDraw(canvas);
drawAutofilledHighlight(canvas);
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
5. If necessary, draw the fading edges and restore layers
6. Draw decorations (scrollbars for instance)
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
// Step 7, draw the default focus highlight
drawDefaultFocusHighlight(canvas);
if (debugDraw()) {
debugDrawFocus(canvas);
}
. . . . . .


自定义View几个关键步骤总结:

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: