Android开发艺术探索 第四章

Android开发艺术探索 第四章

4.1 ViewRoot和DecorView

    ViewRoot对应实现类是ViewRootImpl类,它是连接WindowManager和DecorView的纽带,View的三大流程是由ViewRoot来完成的。

在ActivityThread中,当Activity对象被创建后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联

root = new ViewRootImpl(view.getContext(),display);

root.setView(view,wparams,panelParentView);

    View的绘制流程是从ViewRoot的performTraversals方法开始的,它经过measure、layout、draw三个过程将一个View绘制出来。

performTraversals会依次调用performMeasure、performLayout、performDraw三个方法,然后分别调用measure、layout、draw完成顶级View的流程,这其中又会调用onMeasure、onLayout、onDraw方法对所有子元素进行对应的measure、layout、draw流程,重复这个流程,直到完成整个View树的遍历。

measure:测量View的宽和高,在测量完成后,可以通过getMeasuredWidth和getMeasuredHeight获取测量后的宽高,出了在特殊情况下,这基本等于view的宽高。

layout: 确定View在父容器中的放置位置,也就是View的四个顶点坐标和实际的View的宽高,完成后可以拿到四个顶点top、buttom、left、right,并且可以通过getWidth和getHeight获取最终的宽高。

draw: 负责将View绘制在屏幕上。

DecorView作为顶级View,含有竖直方向的linearLayout,这个里面分为上下两部分(和版本主题有关系)。Activity的setContentView就是添加布局到内容栏content中。

4.2理解MeasureSpec

4.2.1 

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

MeasureSpec由大小和模式组成。有三种可能模式:

* UNSPECIFIED:父对象未对子对象施加任何约束。它可以是任何大小它想要。这种一般用于系统内部,表示一种测量状态

* EXACTLY:父项已确定子项的精确大小。view的最终大小是SpecSize所指定的值,对应match_parent和具体数值。

* AT_MOST:子对象可以任意大到SpecSize指定的大小,不能大于这个值。对应wrap_content

*MeasureSpec封装了从父级传递到子级的布局要求。

*每个度量值表示对宽度或高度的要求。

*

* measurespec作为int实现,以减少对象分配。此类用于对<大小、模式>将元组转换为int。

*/
public static class MeasureSpec {

    private static final int MODE_SHIFT = 30;

    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;



    /** @hide */

    @IntDef({UNSPECIFIED, EXACTLY, AT_MOST})

    @Retention(RetentionPolicy.SOURCE)

    public @interface MeasureSpecMode {}



    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(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,

                                      @MeasureSpecMode int mode) {

        if (sUseBrokenMakeMeasureSpec) {//低于17版本的时候

            return size + mode;

        } else {

            return (size & ~MODE_MASK) | (mode & MODE_MASK);

        }

    }



        //类似于{@link#makeMeasureSpec(int,int)},但任何模式为未指定的规范将自动获得0的大小。较旧的应用程序预计会出现这种情况。

    public static int makeSafeMeasureSpec(int size, int mode) {

        if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {

            return 0;

        }

        return makeMeasureSpec(size, mode);

    }



    //从提供的测量规格中提取模式。

    public static int getMode(int measureSpec) {

        //noinspection ResourceType

        return (measureSpec & MODE_MASK);

    }



    //从提供的测量规格中提取尺寸。

    public static int getSize(int measureSpec) {

        return (measureSpec & ~MODE_MASK);

    }





    static int adjust(int measureSpec, int delta) {

        final int mode = getMode(measureSpec);

        int size = getSize(measureSpec);

        if (mode == UNSPECIFIED) {

            // 不需要调整未指定模式的大小。

            return makeMeasureSpec(size, UNSPECIFIED);

        }

        size += delta;

        if (size < 0) {

            Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust:新尺寸将为负数! (" + size +

                    ") spec: " + toString(measureSpec) + " delta: " + delta);

            size = 0;

        }

        return makeMeasureSpec(size, mode);

    }



    //返回指定度量值的字符串表示形式。

    public static String toString(int measureSpec) {

        int mode = getMode(measureSpec);

        int size = getSize(measureSpec);



        StringBuilder sb = new StringBuilder("MeasureSpec: ");

        if (mode == UNSPECIFIED)

            sb.append("UNSPECIFIED ");

        else if (mode == EXACTLY)

            sb.append("EXACTLY ");

        else if (mode == AT_MOST)

            sb.append("AT_MOST ");

        else

            sb.append(mode).append(" ");

        sb.append(size);

        return sb.toString();

    }

}

4.2.2

MeasureSpec和LayoutParams的关系,

MeasureSpec一般由自己LayoutParams和父容器来定义,对于顶级View(DecorView),他没有父容器,所以只由自身的LayoutParams来确定。对于DecorView来说,它的宽高基本就是屏幕的尺寸,具体代码可以参考DecorView的MeasureSpec的创建过程。具体的规则如下:

1.MATCH_PARENT:精确模式,大小就是窗口的大小。

2.WRAP_CONTENT:最大模式,大小不确定,但是最大不能超过窗口的大小。

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

接下来我们看一下普通View,容器中的View,View的measure测量过程由ViewGroup容器传递过来,先看一下ViewGroup的measureChildWithMargins方法:

protected 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(childWidthMeasureSpec, childHeightMeasureSpec);

}

上述方法会对子元素进行测量,在调用测量之前会通过getChildMeasureSpec获得子元素的MeasureSpec,子元素的MeasureSpec除了与父容器的MeasureSpec和子元素的LayoutParams有关系外,还和View的margin及padding有关

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {

    int specMode = MeasureSpec.getMode(spec);

    int specSize = MeasureSpec.getSize(spec);



    int size = Math.max(0, specSize - padding);



    int resultSize = 0;

    int resultMode = 0;



    switch (specMode) {

    // Parent has imposed an exact size on us

    case MeasureSpec.EXACTLY:

        if (childDimension >= 0) {

            resultSize = childDimension;

            resultMode = MeasureSpec.EXACTLY;

        } else if (childDimension == LayoutParams.MATCH_PARENT) {

            // Child wants to be our size. So be it.

            resultSize = size;

            resultMode = MeasureSpec.EXACTLY;

        } else if (childDimension == LayoutParams.WRAP_CONTENT) {

            // Child wants to determine its own size. It can't be

            // bigger than us.

            resultSize = size;

            resultMode = MeasureSpec.AT_MOST;

        }

        break;



    // Parent has imposed a maximum size on us

    case MeasureSpec.AT_MOST:

        if (childDimension >= 0) {

            // Child wants a specific size... so be it

            resultSize = childDimension;

            resultMode = MeasureSpec.EXACTLY;

        } else if (childDimension == LayoutParams.MATCH_PARENT) {

            // Child wants to be our size, but our size is not fixed.

            // Constrain child to not be bigger than us.

            resultSize = size;

            resultMode = MeasureSpec.AT_MOST;

        } else if (childDimension == LayoutParams.WRAP_CONTENT) {

            // Child wants to determine its own size. It can't be

            // bigger than us.

            resultSize = size;

            resultMode = MeasureSpec.AT_MOST;

        }

        break;



    // Parent asked to see how big we want to be

    case MeasureSpec.UNSPECIFIED:

        if (childDimension >= 0) {

            // Child wants a specific size... let them have it

            resultSize = childDimension;

            resultMode = MeasureSpec.EXACTLY;

        } else if (childDimension == LayoutParams.MATCH_PARENT) {

            // Child wants to be our size... find out how big it should

            // be

            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;

            resultMode = MeasureSpec.UNSPECIFIED;

        } else if (childDimension == LayoutParams.WRAP_CONTENT) {

            // Child wants to determine its own size.... find out how

            // big it should be

            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;

            resultMode = MeasureSpec.UNSPECIFIED;

        }

        break;

    }

    //noinspection ResourceType

    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);

}

子元素的处理逻辑就是根据父容器占用的控件,减去间距padding剩下的可用空间,具体处理逻辑总结可以看表,

其实对于这一节内容,大家熟悉View的布局应该也能好了解,只不过代码实现细节不太清楚。稍微需要注意,当View取固定大小,那么不管父容器的模式是什么,View都会是那么大,不会受父容器影响。

4.3 View 的工作流程

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

4.3.1.measure测量过程

单独的VIew测量自己就可以了,如果是ViewGroup,需要遍历所有子元素的measure测量方法,各个子元素在递归执行这个流程,首先看View的测量

1.View的onMeasure过程

View的onMeasure过程由final类型的measure方法完成的,意味不能继承重写,测量的时候会调用onMeasure方法,所以重点看看这个方法。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),

            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));

}

主要看getDefaultSize方法,获取到宽高设置到测量宽高里面

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;

}

主要需要理解AT_MOST和EXACTLY过程,这个specSize就是View测量后的大小,大部分情况下这个和layout布局后的最终大小是相等的。

UNSPECIFIED主要是用于系统内部测量过程,如果View没有设置背景,那么View的宽度为minWidth这个属性设定的值,如果没有指定,它的默认值是0.如果View设置了背景,那么它的值是这两者之间的最大值,

protected int getSuggestedMinimumWidth() {

    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());

}

需要留意一下背景图的宽度,如果是BitmapDrawable是有原始宽高的,图片的尺寸,但是ShapeDrawable是没有原始宽高的。

自定义控件如果继承View,需要重写onMeasure方法并设置wrap_content时的自身大小,否则就相当于match_parent。我们需要给View指定一个默认的内部宽高,并在wrap_content是设置此宽高即可。具体代码可以参考TextView、ImageView等的源码就可以知道了,针对wrap_content的情形,onMeasure方法均做了特殊处理,

2.ViewGrop的onMeasure过程

ViewGrop需要遍历子元素的measure方法,和VIew不一样的地方,ViewGrop是一个抽象类,因此没有重写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方法进行测量。

ViewGrop没有定义具体的onMeasure测量过程,因为ViewGrop是一个抽象类,具体onMeasure实现需要子类去实现,例如LinearLayout等布局,因为每个布局特性不一样,测量方式也不一样。

接下来看看LinearLayout的onMeasure方法

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

    if (mOrientation == VERTICAL) {

        measureVertical(widthMeasureSpec, heightMeasureSpec);

    } else {

        measureHorizontal(widthMeasureSpec, heightMeasureSpec);

    }

}

线性布局分方向,选个measureVertical看看,源码过长不在贴代码。

final int usedWidth = totalWeight == 0 ? mTotalLength : 0;

measureChildBeforeLayout(child, i, widthMeasureSpec, usedWidth,

        heightMeasureSpec, 0);

final int childWidth = child.getMeasuredWidth();

if (useExcessSpace) {

    lp.width = 0;

    usedExcessSpace += childWidth;

}

if (isExactly) {

    mTotalLength += childWidth + lp.leftMargin + lp.rightMargin

            + getNextLocationOffset(child);

} else {

    final int totalLength = mTotalLength;

    mTotalLength = Math.max(totalLength, totalLength + childWidth + lp.leftMargin

            + lp.rightMargin + getNextLocationOffset(child));

}

主要流程就是遍历各个子元素执行measureChildBeforeLayout,这个方法会调用子元素的measure方法,这样各个子元素完成测量过程,然后系统通过mTotalLength记录总方向的初步高度。高度高开子元素高度、方向上的margin等,最后LinearLayout根据子元素的测量结果给出测量自己的大小。

可以看看LinearLayout测量自己的大小和View的过程是差不多的,主要是自适应的时候是用所有子元素的大小,而View是用自己的背景和本身。

public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {

    final int specMode = MeasureSpec.getMode(measureSpec);

    final int specSize = MeasureSpec.getSize(measureSpec);

    final int result;

    switch (specMode) {

        case MeasureSpec.AT_MOST:

            if (specSize < size) {

                result = specSize | MEASURED_STATE_TOO_SMALL;

            } else {

                result = size;

            }

            break;

        case MeasureSpec.EXACTLY:

            result = specSize;

            break;

        case MeasureSpec.UNSPECIFIED:

        default:

            result = size;

    }

    return result | (childMeasuredState & MEASURED_STATE_MASK);

}

三个流程里面最复杂的measure完成后,就可以通过getMeasureWidth/Height就可以正常的获取到View的测量宽高了,需要注意,measure可能存在多次调用,大部分是调用一次就可以,稳妥起见最好在onLayout方法中或者界面的测量宽高。

因为View的measure方法过程和活动的声明周期不是同步执行,所有无法在onCreate\onStart\onResume中准确获取到测量宽高。

(1)Activity/View:onWindowFocusChanged

这个方法调用的时候是View已经初始化完成,需要留意这个方法可以被多次调用,因为焦点事件变化。

@Override

public void onWindowFocusChanged(boolean hasFocus) {

    super.onWindowFocusChanged(hasFocus);

    if (hasFocus){

        int width=view.getMeasuredWidth();

        int height=view.getMeasuredHeight();

    }

}

(2)View.post

通过post可以将一个线程投递到消息队列末尾,等Looper调用这个线程的时候,View肯定也初始化好了

view.post(new Runnable() {

    @Override

    public void run() {

        int width=view.getMeasuredWidth();

        int height=view.getMeasuredHeight();

    }

});

(3)ViewTreeObserver

当View树的状态发生改变,onGlobalLayout方法会被回调,因此也是个很好的获取机会,同样需要注意,会被调用多次,有改变就会调用

ViewTreeObserver observer=view.getViewTreeObserver();

observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {

    @Override

    public void onGlobalLayout() {

        int width=view.getMeasuredWidth();

        int height=view.getMeasuredHeight();

    }

});

(4)View.measure(int widthMeasureSpec, int heightMeasureSpec)

可以手动进行测量得到高度,需要区分情况

match_parent:直接方法,需要父控件大小

具体的数值:直接调用,例如下面

//100px

int widthMeasureSpec= View.MeasureSpec.makeMeasureSpec(100,View.MeasureSpec.EXACTLY);

int heightMeasureSpec= View.MeasureSpec.makeMeasureSpec(100,View.MeasureSpec.EXACTLY);

view.measure(widthMeasureSpec,heightMeasureSpec);

wrap_content:按照View理论支持的最大值去构造

int widthMeasureSpec= View.MeasureSpec.makeMeasureSpec((1<<30)-1,View.MeasureSpec.EXACTLY);

int heightMeasureSpec= View.MeasureSpec.makeMeasureSpec((1<<30)-1,View.MeasureSpec.EXACTLY);

view.measure(widthMeasureSpec,heightMeasureSpec);

需要注意,其他写法不一定可以保证measure出正确的结果

4.3.2 layout 过程

Layout的作用是ViewGroup用来确定子元素的位置,当ViewGroup的位置被确定后,它在onLayout中会遍历所有子元素并调用其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 oldT = mTop;

    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);

            }

        }

    }

    final boolean wasLayoutValid = isLayoutValid();

    mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;

    mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;

}

首先通过setFrame方法来设置View的四个顶点位置,View位置一旦确定,那么View在父容器的位置也就确定;接着会调用onLayout方法,这个方法的用途是父容器确定子元素的位置,类似onMeasure方法。所有View和ViewGroup均没有实现onLayout方法

接下来看一下 LinearLayout的onLayout方法,实现和onMeasure逻辑类似,我们选择layoutVertical继续看

@Override

protected 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);

    }

}



void layoutVertical(int left, int top, int right, int bottom) {

    final int paddingLeft = mPaddingLeft;

    int childTop;

    int childLeft;



    // Where right end of child should go

    final int width = right - left;

    int childRight = width - mPaddingRight;



    // Space available for child

    int childSpace = width - paddingLeft - mPaddingRight;



    final int count = getVirtualChildCount();



    final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;

    final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;



    switch (majorGravity) {

       case Gravity.BOTTOM:

           // mTotalLength contains the padding already

           childTop = mPaddingTop + bottom - top - mTotalLength;

           break;

           // mTotalLength contains the padding already

       case Gravity.CENTER_VERTICAL:

           childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;

           break;

       case Gravity.TOP:

       default:

           childTop = mPaddingTop;

           break;

    }



    for (int i = 0; i < count; i++) {

        final View child = getVirtualChildAt(i);

        if (child == null) {

            childTop += measureNullChild(i);

        } else 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),

                    childWidth, childHeight);

            childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

            i += getChildrenSkipCount(child, i);

        }

    }

}

layoutVertical的代码逻辑,会遍历所有子元素并调用setChildFrame方法为子元素来指定位置,其中childTop会逐步增大,这样就意味着后面的子元素会被放在考下的位置,符合LinearLayout的特性,setChildFrame调用了子元素的layout,子元素又通过自己的layout方法来确定自己位置,层层传递下去完成整个View树的layout过程

private void setChildFrame(View child, int left, int top, int width, int height) {

    child.layout(left, top, left + width, top + height);

}

setChildFrame中的width和height实际上就是子元素的测量宽高,

final int childWidth = child.getMeasuredWidth();

final int childHeight = child.getMeasuredHeight();

layout方法中会通过setFrame去设置子元素的四个顶点的位置

mLeft = left;

mTop = top;

mRight = right;

mBottom = bottom;

下面来说测量宽高和最终宽高有什么区别

测量时间不一样吗,测量宽高是在measure过程,最终宽高是形成与View的layout过程,两者赋值的时机不同,日常开发中两者基本相等,如果重写layout方法,进行修改,那么他两可以不一样。

4.3.3 draw过程

draw过程就是讲界面绘制到屏幕上,绘制流程如下:

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

(2) 绘制自己(onDraw).

(2) 绘制children (dispatchDraw).

(2) 绘制装饰(onDrawScrollBars).

代码draw如下

public void draw(Canvas canvas) {

    final int privateFlags = mPrivateFlags;

    mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

/*

*绘制遍历执行几个必须执行的绘制步骤

*按照适当的顺序:

*

*1.绘制背景

*2.如有必要,保存画布层以备褪色

*3.绘制视图的内容

*4.画子元素

*5.如有必要,绘制褪色边缘并恢复层

*6.绘制装饰(例如滚动条)

*7.如有必要,绘制默认焦点突出显示

*/

    // Step 1, draw the background, if needed

    int saveCount;

    drawBackground(canvas);

    // 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) {

        // Step 3, draw the content

        onDraw(canvas);

        // 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);

        }

        // Step 6, draw decorations (foreground, scrollbars)

        onDrawForeground(canvas);

        // Step 7, draw the default focus highlight

        drawDefaultFocusHighlight(canvas);

        if (isShowingLayoutBounds()) {

            debugDrawFocus(canvas);

        }

        // we're done...

        return;

    }

    ...

}

dispatchDraw来传递绘制流程,dispatchDraw会遍历所有子元素的Draw方法,一层层传递下去。

setWillNotDraw方法,如果一个View不需要绘制任何内容,可以设置这个标记位为true,系统会默认进行优化。ViewGroup默认启用这个标记位,如果知道一个viewGroup需要通过onDraw来绘制内容的时候,我们需要显示关闭WILL_NOT_DRWA这个标记位

public void setWillNotDraw(boolean willNotDraw) {

    setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);

}

4.4 自定义View

前面讲了View的绘制流程,那么我们可以自定义View实现各种效果。

4.4.1 自定义View的分类

  1. 继承View,重新 onDrew方法:主要重写onDraw实现以下不规则的效果,需要自己支持wrap_content,并且padding也需要自己处理。

  1. 继承ViewGroup派生特殊的Layout:主要是实现自定义布局,需要合适处理ViewGroup的测量、布局两个过程,还有子元素的测量和布局过程。

  2. 继承特定的View(比如TextView):扩展已有View的功能,在已有的View添加自己需要的逻辑。

  3. 继承特定的ViewGroup(比如LinearLayout):扩展已有容器的功能,和3类似。比2少了自己处理测量和布局过程。

4.4.2 自定义View注意事项

  1. 让View支持wrap_content:如果不支持,那么控件在自适应的时候无法达到预期效果。

  2. 让View支持padding:如果不支持,那么padding属性失效。直接继承ViewGroup的控件在测量和布局过程中考虑padding和子元素的margin对其造成的影响,不然导致子元素margin也会失效。

  3. View中尽量不要使用Handler:View本身提供了post系列的方法,完全可以替代Handler的作用。

  4. View中如果有线程或者动画,需要及时停止:参考View#onDetachedFromWindow,当包含此View的Activity退出或者当前View被remove时候,onDetachedFromWindow会被调用,和这个方法对应的是onAttachedToWindow,当包含此View的Activity启动时候,View的onAttachedToWindow会被调用,同时,View变得不可见的时候我们也需要停止动画和动画。如果不及时处理线程和动画,可能造成内测泄漏。

  5. View带有滑动嵌套情况时,需要处理好滑动冲突:如果有滑动冲突,需要合适解决,否则严重影响view的效果。

4.4.3 自定义View示例

1.继承View,重新 onDrew方法

这里是自定义一个圆,会在界面的最中心位置,需要考虑一下View四周的padding就可以了。

@Override

protected void onDraw(Canvas canvas) {

    super.onDraw(canvas);

    int paddingLeft = getPaddingLeft();

    int paddingRight = getPaddingRight();

    int paddingTop = getPaddingTop();

    int paddingBottom = getPaddingBottom();

    int width = getWidth() - paddingLeft - paddingRight;

    int height = getHeight() - paddingTop - paddingBottom;

    int radius = Math.min(width, height);

    canvas.drawCircle(paddingLeft + width / 2f, paddingTop + height / 2f, radius, mPaint);

}

测量的时候需要适配一下wrap_content,简单来说自适应需要安排大小,因为View已经没有子元素了,如果不定具体值,那么他会为零。

@Override

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(200, 200);

    } else if (widthSpecMode == MeasureSpec.AT_MOST) {

        setMeasuredDimension(200, heightSpecSize);

    } else if (heightSpecMode == MeasureSpec.AT_MOST) {

        setMeasuredDimension(widthSpecSize, 200);

    }

}

添加自定义属性,雷士下面这种,定义自己的风格和属性就好了,这个属性是什么格式,reference是ID,dimension是尺寸,color是颜色等可以自己设置。

<?xml version="1.0" encoding="utf-8"?>

<resources>

<declare-styleable name="CircleView">

    <attr name="circle_color" format="color" />

</declare-styleable>

</resources>

然后就是在代码里面使用,从构造的AttributeSet 读取就好了

private void loadAttrs(AttributeSet attrs) {

    if (attrs != null) {

        TypedArray attributes = mContext.obtainStyledAttributes(attrs, R.styleable.BnLoading);

        mColor= attributes.getColor(R.styleable.CircleView_circle_color, Color.RED);

    }

}

最后是在代码里面使用,需要注意使用自定义属性,需要声明xmlns:app="http://scheamas.android.com/apk/res-auto",这个声明,app是自定义的前缀,可以修改自定义,但是CircleView的自定义属性的前缀必须和这里的一致。

<LineraLayout 

xmlns:app="http://scheamas.android.com/apk/res-auto"

>

<CircleView 

app:circle_color="@color/green"

/>

</LineraLayout>

2.继承ViewGroup派生特殊的Layout

这种方法需要合适的处理测量、布局这两个过程,并且同时处理子元素的测量和布局流程,具体可以看看LinearLayout等源码参考,实现抖很复杂。

回顾一下之前HorizontalScrollViewEx的功能,主要是内部子元素可以水平滑动并且还可以进行垂直滑动,这需要处理水平和垂直滑动冲突的问题。

这里主要是看测量和布局过程,先看测量onMeasure,首先会判断是否有子元素,如果没有直接就是宽高都是0,然后判断是不是采用了自适应,如果是就等于所有元素宽度之和,高度等于第一个元素高度。这样写法不规范有两点,1 没有根据根元素中宽高做处理 2没有考虑自己的间距和子元素的间距,因为这些都是会影响宽高的。

其次看一下布局onLayout方法,首先一样先遍历所有的子元素,如果这个控件看可见,需要把它放在合适的位置,从这个放置过程是从左到右的,规范上的问题和上面测量是一样的问题。

4.4.4 自定义View的思想

自定义界面主要是需要熟练的基本功,view的弹性滑动、滑动冲突、绘制原理灯光,这些在加上对控件实施适当的思路,就可以实现看起来很炫酷的自定义控件了。

  • 17
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

时代我西

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值