自定义View的理解

本文详细解析了Android中View的绘制流程,从ViewRoot的performTraversals方法开始,介绍了MeasureSpec的作用及其实现方式,深入探讨了View和ViewGroup在测量过程中的区别与联系,并提供了获取控件宽高的有效方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

View的绘制流程是从ViewRoot的performTraversals方法开始的。

MeasureSpace
代表一个32位的int值,高2位代表SpaceMode,低30位代表SpaceSize。
SpaceMode分三类:
  • ==UNSPECIFLED==:父容器不对View的大小做限制,要多大给多大
  • ==EXACTLY==:父容器已经检测出了View的精确大小,这个模式下View的最终大小就是SpaceSize所给定的值,_相当于LayoutParams 中的match-parent和具体的数值两种。
  • ==AT_MOST==:父容器制定了一个SpaceSize的可用大小,View的大小不能超过这个值,对应warp_content

获取SpaceMode和SpaceSize的方法是:

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
View 的 measure

view的测量只需要通过onMeasure方法就可以完成

==注意==:直接继承View的自定义控件需要重写onMeasure方法并设置warp_content时的自身大小,否则在布局中使用warp_content就相当于使用match_parent。具体实现代码:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        /**
         *  给view制定一个默认的内部宽高,如果SpaceMode为MeasureSpec.AT_MOST然后布局文件中又设置为wrap_content,
         *  则使用这个默认宽高,不然view的效果就会是match_parent的效果
         */

        int mWidth = 50;
        int mHeight = 50;

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(mWidth,mHeight);
        }else if(widthMode == MeasureSpec.AT_MOST ){
            setMeasuredDimension(mWidth,heightSize);
        }else if (heightMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(widthSize,mHeight);
        }
    }

根据项目的实际情况而定,可以参考TextView的源码

ViewGroup的measure

ViewGroup除了要完成自己的measure过程,还会遍历去调用所有子view的measure方法。所以ViewGroup是通过measureChildren方法完成测量的。它的代码如下:

    /**
     * Ask all of the children of this view to measure themselves, taking into
     * account both the MeasureSpec requirements for this view and its padding.
     * We skip children that are in the GONE state The heavy lifting is done in
     * getChildMeasureSpec.
     *
     * @param widthMeasureSpec The width requirements for this view
     * @param heightMeasureSpec The height requirements for this 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);
            }
        }
    }

然后是measureChild去测量具体的每一个view:

    /**
     * Ask one of the children of this view to measure itself, taking into
     * account both the MeasureSpec requirements for this view and its padding.
     * The heavy lifting is done in getChildMeasureSpec.
     *
     * @param child The child to measure
     * @param parentWidthMeasureSpec The width requirements for this view
     * @param parentHeightMeasureSpec The height requirements for this 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);
    }

最后又回到了view的measure方法中。

一个常见的问题,经常需要子Activity启动的时候去获取一些控件的宽高,但是在onCreate、onStart、onResume中都无法正确获取到。因为view的measure方法和Activity的生命周期不是同步的。所以可以通过下面这四种方法来获取:

  1. onWindowFocusChanged
    public void onWindowFocusChanged(boolean hasFocus){
        super.onWindowFocusChanged(hasFocus);
        if(hasFocus){
            int width = view.getMeasuredWidth();
        }
    }

2.view.post(runnable)

void start(){
    super.start();
    view.post(new Runnable(){
        @Override
        public void run(){
            int width = view.getMeasuredWidth();
        }
    }
});

3.ViewTreeObserver

void start(){
    super.start();

    ViewTreeObserver observer = view.getViewTreeOberver();
    observer.addOnGlobalLyoutListener(new OnGlobalLayoutListener(){

        @Override
        public void onGlobalLayout(){
            view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
            int width = view.getMeasuredWidth();
        }

    })
}

4.手动计算view.measure(int widthMeasureSpec,int heightMeasureSpec)

layout
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

        View view = getChildAt(0);

        //左边与父容器的距离
        int mLeft = 0;
        //上边与父容器的距离
        int mTop = 0;
        //右边就是view的宽
        int mRight = mLeft+view.getMeasuredWidth();
        //下边就是view的高
        int mBottom = mTop+view.getMeasuredHeight();

        //因为上边和左边和父容器的边距都是0 ,所以这个view就是在父容器的左上角
        view.layout(mLeft,mTop,mRight,mBottom);

    }

其实layout也可以控制view的宽高,比如将上面的代码修改如下:


    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

        View view = getChildAt(0);

        //左边与父容器的距离
        int mLeft = 0;
        //上边与父容器的距离
        int mTop = 0;
        //右边就是view的宽
        int mRight = mLeft+view.getMeasuredWidth()*4;
        //下边就是view的高
        int mBottom = mTop+view.getMeasuredHeight()*4;

        //因为上边和左边和父容器的边距都是0 ,所以这个view就是在父容器的左上角
        view.layout(mLeft,mTop,mRight,mBottom);

    }

这个view的宽高就变为原来的4倍。

draw

draw的过程相对来说就比较简单,因为全是自己的逻辑。自己想实现什么效果就画什么,主要是能理解canvas的各种draw方法。

view有一个特殊的方法setWillNotDraw,如果一个view不需要绘制任何内容,就设置为true,系统就会进行相应的优化。默认情况下view的这个方法是false的,但是viewgroup是设置为true,所以当一个viewgroup要调用onDraw来绘制内容时需要设置setWillNotDraw为false。这需要在构造方法中设置,代码如下:

public class MyViewgroup extends ViewGroup {
    public MyViewgroup(Context context) {
        super(context);
    }

    public MyViewgroup(Context context, AttributeSet attrs) {
        super(context, attrs);
         //启用viewgroup的onDraw方法
        setWillNotDraw(false);
    }

对于直接继承view的自定义控件来说,如果不在onDraw方法中处理padding,那么padding属性就无法生效,代码处理如下:

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //计算控件的宽高时要把padding值考虑进去
        paddingLeft = getPaddingLeft();
        paddingTop = getPaddingTop();
        paddingRight = getPaddingRight();
        paddingBottom = getPaddingBottom();
        Rect rect = new Rect();
        rect.left = 0 + paddingLeft;
        rect.top = 0 + paddingTop;
        rect.right = width - paddingRight;
        rect.bottom = height - paddingBottom;
        canvas.drawRect(rect,mPaint);
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值