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

来源:互联网 发布:京宽网络24客服电话 编辑:程序博客网 时间:2024/06/03 20:56

这篇博客有点长,但我想讲得还是比较清晰的,希望能坚持看完。

一、几个重要的概念:
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某种取值下测得的规格大小,具体如下:

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创建规则如下:

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

子View的MeasureSpec
至于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 layers6. 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几个关键步骤总结:

自定义View关键步骤

阅读全文
'); })();
0 0
原创粉丝点击
热门IT博客
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 疾速追杀3在线超清 杀漠 杀犬 男子药杀百余只狗 杀狗 梦见杀狗 异烟肼杀狗原理 异烟肼杀狗 梦到杀狗 杀狗图片 剧本杀训狗人 自制杀狗药教程 梦见别人杀狗 梦见自己杀狗 杀狗叫声mp3 梦见杀狗见血 沙皇杀了狗头会说什么 梦见别人把狗杀了 狮子王中土狗为什么杀刀疤 杀狗劝妻 梦见杀狗是什么征兆 梦见自己杀狗是什么征兆 梦到杀狗是什么意思 梦见别人杀狗自己在看 杀狗神器 怎么杀狗方法手段图解 梦见杀狗是什么意思 杀狗的过程 自己养的宠物狗可以杀来吃吗 异烟肼为什么能杀狗 梦见别人用很残忍的方法杀狗 杀猪 梦见杀猪 杀猪菜 梦见杀猪见血 上辈子杀猪这辈子教书 梦到杀猪 梦见杀猫 杀猫 猫有杀主人的意识 猫杀主人的征兆