自定义View基础知识

系列文章:

一、简析Window、Activity、DecorView以及ViewRoot关系

二、View的工作原理

一、android坐标系

android坐标系和数学坐标系不同,所以要单独记录一下。

1、屏幕坐标系

在Android中,将屏幕的左上角的顶点作为Android坐标系的原点,这个原点向右是X轴正方向,原点向下是Y轴正方向。触控事件中,使用MotionEvent.getRawX()和MotionEvent.getRawY()方法获取的坐标就是以这个坐标系下的坐标值。

2、视图坐标系 

在日常开发中我们接触最对的就是视图坐标系了,视图坐标系描述的是子控件在父控件中相对位置。视图坐标系是以控件的父控件的左上角为坐标原点,从原点出发水平向右为x轴正方向,垂直向下为y轴正方向。

1)、View提供下面方法,通过它们可以获得View到其父控件(ViewGroup)的距离:

  • getTop():获取View自身顶边到其父布局顶边的距离
  • getLeft():获取View自身左边到其父布局左边的距离
  • getRight():获取View自身右边到其父布局左边的距离
  • getBottom():获取View自身底边到其父布局顶边的距离
  • getX():返回值为getLeft()+getTranslationX(),当setTranslationX()时getLeft()不变,getX()变
  • getY():返回值为getTop()+getTranslationY(),当setTranslationY()时getTop()不变,getY()变

2)、MotionEvent 提供下面方法,可以获取触摸点的坐标

我们看上图那个深蓝色的点,假设就是我们触摸的点,我们知道无论是View还是ViewGroup,最终的点击事件都会由onTouchEvent(MotionEvent event)方法来处理,MotionEvent也提供了各种获取焦点坐标的方法:

  • getX():获取点击事件距离控件左边的距离(注意不是相对于父控件距离,是相对于自己左边的距离),即视图坐标
  • getY():获取点击事件距离控件顶边的距离(注意不是相对于父控件距离,是相对于自己顶边的距离),即视图坐标
  • getRawX():获取点击事件距离整个屏幕左边距离,即绝对坐标
  • getRawY():获取点击事件距离整个屏幕顶边的的距离,即绝对坐标

注意:

View中的getX()、getY()方法只是与MotionEvent中的getX()、getY()方法只是重名而已,并不是一个。

二、MeasureSpec类

1 、简介

MeasureSpec其实就是尺寸和模式通过各种位运算计算出的一个整型值。高2位表示测量模式,低30位表示尺寸

2、MeasureSpec类3种测量模式

1)、UNSPECIFIED(0)

不限定。意思就是,子View想要多大,我就可以给你多大,你放心大胆 的measure吧,不用管其他的。也不用管我传递给你的尺寸值。(其实Android高版本中推荐,只要是这个模式,尺寸设置为0)很少使用,只有在ScrollView和NestedScrollView中看到。

    protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                lp.topMargin + lp.bottomMargin, MeasureSpec.UNSPECIFIED);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

上面是NestedScrollView的measureChildWithMargins方法,childHeightMeasureSpec 就是使用UNSPECIFIED。因此在NestedScrollView中使用RecyclerView时会导致RecyclerView加载所有item数据,RecyclerView的复用就失效了。

2)、EXACTLY(1<<30)

精确的。父容器已经测量出View需要的宽高,View直接使用父容器给出的宽高即可。match_parent和设置具体的宽高就是EXACTLY

3)、AT_MOST(2<<30)

最多的。父容器指定一个可用大小,View的大小不能大于这个值,具体大小看具体View的实现。wrap_content就是AT_MOST。wrap_content和match_parent传入的尺寸大小是一样的,所以子View在wrap_content时必须根据View中的内容测试自己的需要的宽高,然后调用setMeasuredDimension(width, height)设置。否则效果就和match_parent一样了

3、提供方法

1)、获取模式

MeasureSpec.getMode(widthMeasureSpec);

2)、获取大小

MeasureSpec.getSize(widthMeasureSpec);

3)、尺寸和模式转化为MeasureSpec

MeasureSpec.makeMeasureSpec(size, mode);

三、如何确定View的MeasureSpec?

子View的MeasureSpec由子View的LayoutParams(包含子View的宽高)和父容器的MeasureSpec共同决定。(子view不是设置多大就多大,比如父容器大小小于子View大小。)

ViewGroup测试子View代码:

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

上面的getChildMeasureSpec()就是计算子View的MeasureSpec。代码如下:

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

spec表示父View的MeasureSpec,padding表示父View中已经使用的大小,childDimension表示子View的layoutParam中指定的宽或者高。上面代码大致逻辑是根据父View的测量模式和子View的layoutParam中宽或高(wrap_contgent、match_parent以及确定值三种情况)确定MeasureSpec。具体规则如下表:

parentSize是指父控件的剩余大小。

四、View的工作过程

View的工作过程是指measure(测量)、layout(布局)、draw(绘制)三大过程。measure确定View的测量宽高,layout确定View的最终宽高和四个顶点的位置,draw将View绘制在屏幕上。注意:measure确定的是测量宽高,layout确定最终宽高,但几乎所有情况下测量宽高和最终宽高相等,getMeasureHeight是测量高度,getHeight是最终高度。

1、measure过程

setMeasureDimension方法是最终设置View宽高的方法,只有调用了这个方法才把宽高设置到View。

1)、View的测量过程

measure过程由View的measure方法完成,measure方法中会调用onMeasure方法,所以最终测量在onMeasure中完成。onMeasure如下:

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

上面的宽高是通过getDefaultSize获取的,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;
    }

UNSPECIFIED的情况可以不考虑。AT_MOST和EXACTLY都是使用父View传入的测量尺寸。因此直接继承View的自定义控件需要重写onMeasue方法,wrap_content时根据View特点自己计算宽高,match_parent和具体值时使用父View传入的测量尺寸。

2)、ViewGroup的测量过程

ViewGroup继承View,没有重写measure()和onMeasure。自定义ViewGroup时要重写onMeasue方法,wrap_content时根据布局特点自己计算宽高,match_parent和具体值时使用父View传入的测量尺寸。除了完成自己的measure,还需要完成子View的measure,在onMeasure中要调用measureChildren。ViewGroup中提供了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);
            }
        }
    }

即循环遍历所有子View调用measureChild测量,measureChild代码如下:

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

getChildMeasureSpec是计算子View的MeasureSpec,child.measure()会调用到View的onMeasure()

2、layout过程

layout过程是ViewGroup确定子View的位置。因此View没有layout过程,View的位置是在ViewGroup中确定的。layout方法确定自己的位置,然后会去调用onLayout确定子View的位置。ViewGroup是一个抽象类,onLayout是其唯一的抽象方法,因此自定义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);
                }
            }
        }

        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
    }

大致流程如下:

1)、通过setFrame方法设定View四个顶点的位置,View在父View中的位置就确定了

2)、调用onLayout,父容器确定子View的位置。具体如何实现和ViewGroup的布局特性有关

3、draw过程

最终绘制过程是在View中,因此直接继承View的自定义View时要实现onDraw,自定义ViewGroup不用实现onDraw。ViewGroup中已经实现了dispatchDraw,ViewGroup在draw方法中调用dispatchDraw绘制子View。

ViewGroup调用draw方法绘制子View,draw方法先绘制自己,然后绘制子View。draw方法的源码如下:

    public void draw(Canvas canvas) {
        final int privateFlags = mPrivateFlags;
        final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
                (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
        mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

        /*
         * Draw traversal performs several drawing steps which must be executed
         * in the appropriate order:
         *
         *      1. Draw the background
         *      2. If necessary, save the canvas' layers to prepare for fading
         *      3. Draw view's content
         *      4. Draw children
         *      5. If necessary, draw the fading edges and restore layers
         *      6. Draw decorations (scrollbars for instance)
         */

        // Step 1, draw the background, if needed
        int saveCount;

        if (!dirtyOpaque) {
            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
            if (!dirtyOpaque) onDraw(canvas);

            // Step 4, draw the children
            dispatchDraw(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);

            // we're done...
            return;
        }

        ......
    }

大致流程如下:

1)、绘制背景--drawBackground(canvas);

2)、绘制自己的内容--onDraw(canvas);

3)、绘制子View--dispatchDraw(canvas);

4)、绘制装饰foreground or scrollbars--onDrawForeground(canvas);

通过dispatchDraw方法一步一步绘制子View,dispatchDraw在View中是一个空实现,具体实现在ViewGroup中。代码如下:

    protected void dispatchDraw(Canvas canvas) {
        ......
        for (int i = 0; i < childrenCount; i++) {
            while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
                final View transientChild = mTransientViews.get(transientIndex);
                if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
                        transientChild.getAnimation() != null) {
                    more |= drawChild(canvas, transientChild, drawingTime);
                }
                transientIndex++;
                if (transientIndex >= transientCount) {
                    transientIndex = -1;
                }
            }

            final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
            final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
            if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
                more |= drawChild(canvas, child, drawingTime);
            }
        }
      ......
    }

通过drawChild绘制子View

    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        return child.draw(canvas, this, drawingTime);
    }

总结:

       1、自定义ViewGroup和View要重写onMeasue,wrap_content时根据自己特点计算宽高,match_parent和具体值时使用父View传入的测量尺寸。ViewGroup还要在onMeasure中调用measureChildren测量子View的尺寸。

       2、ViewGroup要重写onLayout,通过View.layout设置子View的位置。View中不需要重写onLayout。

       3、ViewGroup中不需要重写onDraw,直接继承View的需要重写onDraw绘制界面。

五、怎么在Activity中获取View的宽高

在onCreate、onStart、onResume中都无法正确得到某个View的宽高,因为Activity的生命周期和View的measure不是同步执行的。

1、onWindowFocusChanged

onWindowFocusChanged会被调用多次,Activity的窗口得到或者失去焦点时都会调用该方法,即Activity暂停或退出和Activity继续执行或者新建都会调用该方法。因此最好判断得到焦点时获取View的大小,没有焦点获取没有意义。

    public void onWindowFocusChanged(boolean hasFocus) {
        Log.d(TAG, "onWindowFocusChanged>>" + hasFocus);
        if(hasFocus) {
            // 获取View的宽高
            Log.d(TAG, "onWindowFocusChanged flowLayout>>width" + flowLayout.getWidth() + ", height:" + flowLayout.getHeight()
                    + ", measuWidth:" + flowLayout.getMeasuredWidth() + ", measuHeight:" + flowLayout.getMeasuredHeight());
        }
    }

2、用post将一个runnable放到消息队列的尾部,Looper调用runnable时View已经初始化完成了。

        flowLayout.post(new Runnable() {
            @Override
            public void run() {
                // 获取View的宽高
                Log.d(TAG, "post flowLayout>>width" + flowLayout.getWidth() + ", height:" + flowLayout.getHeight()
                        + ", measuWidth:" + flowLayout.getMeasuredWidth() + ", measuHeight:" + flowLayout.getMeasuredHeight());
            }
        });

3、ViewTreeObserver

当View树的状态发生改变或者View树内部的View可见性发生改变,onGlobalLayout方法将被回调。但是View状态改变时onGlobalLayout回调多次,所以获取宽高后就调用flowLayout.getViewTreeObserver().removeOnGlobalLayoutListener(this);

        flowLayout.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                flowLayout.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                // 获取View的宽高
                Log.d(TAG, "GlobalLayoutListener flowLayout>>width" + flowLayout.getWidth() + ", height:" + flowLayout.getHeight()
                        + ", measuWidth:" + flowLayout.getMeasuredWidth() + ", measuHeight:" + flowLayout.getMeasuredHeight());
            }
        });

4、手动调用view.measure(widthMeasureSpec,heightMeasureSpec)测量后获取

总结:多个View需要获取宽高时建议使用onWindowFocusChanged,一个View时使用ViewTreeObserver

六:怎么在自定义View中获取自己或者子View的宽高?

1、在onSizeChanged方法中获得

这里可以获取自己的测量宽高,也可以获取自己的宽高。但是不能获取子控件的宽高,只能获取子控件的测量宽高,因为还没有调用子View 的layout。下面源码可以验证,setFrame是在onLayout之前调用的,即onSizeChanged在onLayout前调用。

    public void layout(int l, int t, int r, int b) {
        ......

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

在setFrame 中调用sizeChange

    protected boolean setFrame(int left, int top, int right, int bottom) {
        boolean changed = false;

        if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
            changed = true;

            // Remember our drawn bit
            int drawn = mPrivateFlags & PFLAG_DRAWN;

            int oldWidth = mRight - mLeft;
            int oldHeight = mBottom - mTop;
            int newWidth = right - left;
            int newHeight = bottom - top;
            boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);

            // Invalidate our old position
            invalidate(sizeChanged);

            mLeft = left;
            mTop = top;
            mRight = right;
            mBottom = bottom;


            if (sizeChanged) {
                sizeChange(newWidth, newHeight, oldWidth, oldHeight);
            }
        ......
     }

sizeChange中直接嗲用onSizeChanged,具体如下:

    private void sizeChange(int newWidth, int newHeight, int oldWidth, int oldHeight) {
        onSizeChanged(newWidth, newHeight, oldWidth, oldHeight);

        rebuildOutline();
    }

 2、onMeasure

在onMeasure方法的最后获取测量宽高,因为这时自己和子View都完成了测量

七、几个重要方法

1、invalidate:可以触发onDraw方法的调用,必须在主线程调用

2、postInvalidate:可以触发onDraw方法的调用,主线程子线程都可以调用

3、requestLayout: 子View调用requestLayout方法,会标记当前View及父容器,同时逐层向上提交,直到ViewRootImpl处理该事件,ViewRootImpl会调用三大流程,从measure开始,对于每一个含有标记位的view及其子View都会进行测量、布局、绘制。

4、onFinishInflate: View全部加载完成调用,在自定义ViewGroup时可以获取子View的引用,不能获取子View的宽高。

八、自定义控件支持padding和margin

1、自定义View

不支持padding的问题就是xml中加了padding没有效果,没有其他问题了。但是如果测量没有考虑padding,绘制的时候又考虑了padding就会出错。

自定义View时如果要支持padding的话,需要在onMeasure中wrap_content时考虑padding,在onDraw方法中绘制时处理padding。margin是由父控件实现,所以自定义View不用考虑margin。

1)、onMeasure中wrap_content时处理padding

代码如下。

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);

        if (widthSpecMode != MeasureSpec.EXACTLY) {
            // wrap_content时
            widthMeasureSpec = MeasureSpec.makeMeasureSpec(mDefaultWidth + getPaddingLeft() + getPaddingRight(), MeasureSpec.EXACTLY);
        }
        if (heightSpecMode != MeasureSpec.EXACTLY) {
            // wrap_content时
            heightMeasureSpec = MeasureSpec.makeMeasureSpec(mDefaultHeight + getPaddingTop() + getPaddingBottom(), MeasureSpec.EXACTLY);
        }

        setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec));
    }

wrap_content计算宽高时要把padding加上,因为padding可以认为是控件的一部分,不加上大小不对。

2)onDraw时处理padding

        measureWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
        measureHeight = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
        mRroundConerRadius = measureHeight * 0.5f;

        mCircleButtonRadius = mRroundConerRadius - dp2px(1);
        mCircleButtonX = measureWidth - mRroundConerRadius;
        mCircleButtonY = getMeasuredHeight() * 0.5f;

        left = getPaddingLeft();
        top = getPaddingTop();

ondraw时会使用上面的measureWidth和left等参数。

2、自定义ViewGroup

自定义ViewGroup时需要在onMeasure和onLayout方法中处理自身padding和子View的margin

场景是自定义一个垂直方向的LinearLayout。有下面几种情况:

1)、onMeasure测试时没有考虑margin

测量代码如下:

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        // 设置为match_parent或者具体值
        if (widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY) {
            measureChildren(widthMeasureSpec, heightMeasureSpec);
            setMeasuredDimension(widthSize, heightSize);
        }
    }

上面measureChilren最终会执行ViewGroup中的measureChild,代码如下。

    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的测量中只计算了padding,没有计算margin。这样会有什么问题呢?

当我们在该控件中添加子控件TextView,宽度是match_parent,TextView中填入达到换行的字数,设置一定的magin。布局文件如下。

<com.study.androidother.CustomView.view.MyLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="text1text1text1text1text1text1text1text1text1text1text1text1text1"
        android:layout_marginLeft="20dp"
        android:textSize="16dp"/>
</com.study.androidother.CustomView.view.MyLinearLayout>

布局的预览效果如下:

上面红色箭头处就是因为测量时没有计算margin,布局中添加margin后把控件挤到屏幕外面去了,挤出屏幕外的TextView的内容也不能显示了。如果在测量时减去了margin就不会出现上面的效果。

更改测量代码如下。

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        if (widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY) {
            int size = getChildCount();
            for (int i = 0; i < size; ++i) {
                final View child = getChildAt(i);
                if (child.getVisibility() != View.GONE) {
                    measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                }
            }
            setMeasuredDimension(widthSize, heightSize);
        } 
    }

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

measureChildWithMargins中计算了margin。现在布局文件效果如下。

现在就没有把TextView挤到屏幕外面去,TextView中的所有内容都正常显示出来了。

2)、onMeasure测试时没有考虑padding

测量代码如下:

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        if (widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY) {
            int size = getChildCount();
            for (int i = 0; i < size; ++i) {
                final View child = getChildAt(i);
                if (child.getVisibility() != View.GONE) {
                    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
                    final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                            lp.leftMargin + lp.rightMargin, lp.width);
                    final int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
                            lp.topMargin + lp.bottomMargin, lp.height);
                    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
                }
            }
            setMeasuredDimension(widthSize, heightSize);
        } 
    }

上面代码测量子View时只考虑了margin,没有考虑padding。布局文件预览效果和上面一样。 把TextView控件挤到屏幕外面去了,挤出屏幕外的TextView的内容也不能显示了。

3)、onLayout中怎么考虑padding和margin

自定义一个垂直方向的LinearLayoutde的onLayout代码如下。

    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int topPositon = getPaddingTop(); // 考虑padding
        int paddingLeft = getPaddingLeft(); // 考虑padding
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
            // 注意:paddingLeft是父控件布局设置的padding。lp.leftMargin是子控件布局设置的。
            int childLeft = paddingLeft + lp.leftMargin;
            int childTop = topPositon + lp.topMargin;
            child.layout(childLeft, childTop,
                    childLeft + child.getMeasuredWidth(),
                    childTop + child.getMeasuredHeight());
            topPositon += lp.topMargin + lp.bottomMargin + child.getMeasuredHeight();
        }
    }

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值