自定义View学习笔记03—View的工作原理简介
2017-12-25 16:55
323 查看
这篇博客有点长,但我想讲得还是比较清晰的,希望能坚持看完。
一、几个重要的概念:
1、MeasureSpec概述:
注意:这里测出来的Width/Height并不一定是最终的宽高,后面会详细解释原因。
2、MeasureSpec含义:
在测量的时候,子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的测量:
简单的理解:getDefaultSize()返回的大小就是specSize,因为UNSPECIFIED模式我们通常用不上,AT_MOST模式下什么也没有做,EXACTLY模式下result=specSize。而这个specSize就是View测量后的大小,但不一定是最终的大小(虽然大概率是最终大小,但小部分情况下不是)。
再来看看getSuggestedMinimumWidth()和getSuggestedMinimumHeight()的源码:
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方法。具体如下:
针对自定义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:
意思很显然,就是在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方法很简单:
对于LinearLayout布局,只有两种方式要么是vertical竖直方向,要么是horizontal水平方向,因此就处理两个方向上的测量。
对于measureVertical和measureHorizontal方法都很长,几百行,在此不做展示。核心思路是:在两个方法里面会遍历所有的子View并对每一个子View执行measureChildBeforeLayout方法:
measureChildBeforeLayout方法里面会调用父类ViewGroup的measureChildWithMargins方法,获取子View的childWidthMeasureSpec和childHeightMeasureSpec参数,然后在调用父类View的measure方法进行测量,并调用onMeasure方法,至此又回到我们最开始的分析:
measureVertical方法里面有一个用于记录累计高度/宽度的参数:mTotalLength,每测量一个子View的高度,就对它执行一次加法,每次增加的高度包括子View的高度及其marginTop/Bottom以及父容器的paddingTop/Bottom。完成对子View的测量后,View还要对自身进行测量。
最终测量完成后,通过getMeasuredWdith/Height方法拿到View的测量宽高。但某些极端情况下,View的measure测量要反复执行数次才能最终确定,因此我们自定义的时候最好是在OnLayout方法里面去获取View的宽高。
RelativeLayout的测量过程与此类似,但更复杂,在此不累述。
五、获取View宽高的几种方法
A、在Activity中复写onWindowFocusChanged方法并获取宽高:
B、调用View.post/postDelayed方法。后者线程安全。
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方法具体实现如下:
大致流程是:
1、通过setFrame方法设定view的四个顶点的位置,即初始化mLeft、mRight、mTop、mBottom,确定了这四个值,就确定了View在父容器中的位置。
2、调用onLayout方法让父容器确定子View的位置。
值得注意的是:
1、ViewGroup的onLayout方法是抽象的:
由继承他的子View(LinearLayout/RelativeLayout/FramLayout等)具体实现;
2、View的onLayout方法是空的,什么也没做:
同样由继承他的子View如TextView,ImageView等具体实现。
我们常用的LinearLayout的OnLayout方法如下:
很明显的,OnLayout依照Vertical和Horizontal两种布局方式分别布局。这里仍以layoutVertical方法作分析:
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方法的区别:
我们先分别看他们的实现方式:
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的由来:
方法child.getMeasuredWidth来自于父类View.class。我们可以看看这个方法的实现:
其中mMeasuredWidth在View.class的setMeasuredDimensionRaw方法中被赋值:
该方法被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的过程比前两个都要简单,它主要是将布局视图绘制到屏幕上,遵循以下几个步骤:
自定义View几个关键步骤总结:
一、几个重要的概念:
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几个关键步骤总结:
相关文章推荐
- iOS学习笔记-056.自定义View03——圆形头像
- Android自定义View学习笔记03
- (原创)c#学习笔记08--面向对象编程简介01--面向对象编程的含义03--静态和实例类成员
- 自定义View学习笔记(一)
- 自定义View学习笔记02—View的几个重要方法
- iOS Programming 学习笔记 - 03 UITableView和UITableViewController
- (转)Qt Model/View 学习笔记 (一)——Qt Model/View模式简介
- iOS学习笔记-057.自定义View04——刷帧动画
- Android 学习笔记 初学自定义viewgroup
- Android自定义view学习笔记01
- 自定义View的onMeasure、onDraw、BitmapShader等等笔记__学习笔记
- ASP.NET MVC 学习笔记-7.自定义配置信息 ASP.NET MVC 学习笔记-6.异步控制器 ASP.NET MVC 学习笔记-5.Controller与View的数据传递 ASP.NET MVC 学习笔记-4.ASP.NET MVC中Ajax的应用 ASP.NET MVC 学习笔记-3.面向对象设计原则
- 自定义VIEW(学习笔记三)-基本图形的绘制
- Qt Model/View学习笔记之一简介篇
- 安卓学习笔记--- Android自定义View(CustomCalendar-定制日历控件)
- 自定义View学习笔记08—Path基本操作
- ios学习笔记----实现一个带滑动手势的tabBarViewController,并可自定义tabBar
- 学习笔记 Tianmao 篇 recyclerView 辅助的RecycleAdapterImpl类(适配自定义home二型)
- android自定义view学习笔记1