我是靠谱客的博主 健壮钢笔,最近开发中收集的这篇文章主要介绍Android自定义View——onMeasure,onLayout,onDraw的作用 View的绘制流程,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

 View的绘制流程

一个View从创建到最终绘制出来,有三个方法是不得不提到的,那就是onMeasure测量,onLayout定位,onDraw绘制

onMeasure

对于一个View绘制前,首先需要测量出来这个View的宽高,而这步工作就是由onMeasure完成的了。

//view测量宽高的方法
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //将计算出来的宽高传入setMeasureDimension方法中完成测量
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

 setMeasureDimension方法通过传入的值计算出绝对宽高,并最终调用setMeasureDimensionRaw方法进行赋值

所以View的宽高测量关键就在于onMeasure方法中,我们只需要在该方法中计算出宽高,并调用setMeasureDimension方法即可完成测量步骤。

而对于宽高的计算,Android提供了一个非常精简有效的类MeasureSpec用于传递宽高信息的

MeasureSpec主要是三个方法

//用于父布局计算好子布局的宽高之后,用这个方法生成对应的int值传给子布局的onMeasure方法
//这个int值实际上是一个32位的二进制,前两位用于储存MODE,后30位用于储存SIZE
public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                          @MeasureSpecMode int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }

//用于解析宽高的MODE
public static int getMode(int measureSpec) {
            //noinspection ResourceType
            return (measureSpec & MODE_MASK);
        }

//用于解析宽高的SIZE
public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }

MeasureSpec类中给出了三种MODE,分别是

  • EXACTLY     精确值模式,对于match_parent(父控件的尺寸值)或者是具体的尺寸值
  • AL_MOST    最大值模式,对应wrap_content,即View宽高随着子控件或者内容的变化而变化,只要不超出父控件给出的最大宽高即可
  • UNSPECIFIED   无限制模式, 对应XML设置宽高为0或者未设置宽高时,一般自定义View用不到

而View的onMeasure方法默认只支持EXACTLY模式,即当你的自定义view不需要使用wrap_content来设定宽高时,可以选择不重写onMeasure方法,否则必须重写该方法进行宽高设置

这里可以这么理解,如果宽高从一开始(即xml定义时)就是确定好并不会改变,那么就不需要去自己重写测量了,如果你的宽高是动态改变的,是根据你的绘制内容而变化的,那么你就需要通过重写onMeasure方法去告诉程序你所需要的宽高值。

下面上一段重写onMeasure方法的代码

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        var widthResult = 0
        var heightResult = 0
        //获取宽高对应的mode
        var widthMode = MeasureSpec.getMode(widthMeasureSpec)
        var heightMode = MeasureSpec.getMode(widthMeasureSpec)
        //获取宽高对应的size
        var widthSize = MeasureSpec.getSize(widthMeasureSpec)
        var heightSize = MeasureSpec.getSize(heightMeasureSpec)
        when(widthMode){
            MeasureSpec.EXACTLY -> {
                //此时的widthSize是XML设定的精确值
                widthResult = widthSize
            }
            MeasureSpec.AT_MOST -> {
                //模拟一个当前绘制内容总宽度
                var currentWidth = 200
                //此时widthSize是父控件允许该控件的最大宽度
                //取二者最小值,即可保证控件跟随内容变化却又不超出父控件
                widthResult = min(widthSize, currentWidth)
            }
            MeasureSpec.UNSPECIFIED -> {
                //模拟一个当前绘制内容总宽度
                var currentWidth = 200
                //直接根据内容设定宽度
                heightResult = currentWidth
            }
        }
        when(heightMode){
            MeasureSpec.EXACTLY -> {
                //此时的heightSize是XML设定的精确值
                heightResult = heightSize
            }
            MeasureSpec.AT_MOST -> {
                //模拟一个当前绘制内容总高度
                var currentWidth = 200
                //此时heightSize是父控件允许该控件的最大高度
                //取二者最小值,即可保证控件跟随内容变化却又不超出父控件
                heightResult = min(heightSize, currentWidth)
            }
            MeasureSpec.UNSPECIFIED -> {
                //模拟一个当前绘制内容总高度
                var currentHeight = 200
                //直接根据内容设定高度
                heightResult = currentHeight
            }
        }
        setMeasuredDimension(widthResult, heightResult)
    }

上面便是自定义view的onMeasure重写代码了,而如果用户需要自定义的是ViewGroup,那便不一样了,我们先看一下参考LinearLayout类的onMeasure方法。

void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
        
        ......

        //遍历子View
        for (int i = 0; i < count; ++i) {
            final View child = getVirtualChildAt(i);

            ......
            //当高度是精确值模式时,直接设置高度
            if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
                
                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);

                ......

            } else {
                
                //当宽高为wrap_content时

                ......
                //测量每一个子View的宽高
                //该方法最终调用child.measure
                measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                        heightMeasureSpec, usedHeight);

                ......

                final int childHeight = child.getMeasuredHeight();

                ......
                
                final int totalLength = mTotalLength;
                //计算每一个子View的高度以及margin之和
                mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));

                ......

        }

        ......

        int heightSize = mTotalLength;

        heightSize = Math.max(heightSize, getSuggestedMinimumHeight());

        int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);

        ......

        //设置自身
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                heightSizeAndState);

        ......
    }

LinearLayout有两个测量方法,分别对应纵向和横向两种情况,这里只贴出measureVertical的部分代码,感兴趣的可以去详细研究一下。可以看到

  1. 当高度为精确值或者match_parent时,LinearLayout可以直接设置高度。
  2. 当高度为wrap_content时,首先需要遍历所有子控件,通过measure依次测量他们的高度,再根据他们的高度计算出自身的高度。

总结:

  1. 当测量模式为EXACTLY(即精确值或match_parent)时,直接设置宽高(此时可以不重写onMeasure方法)
  2. 当测量模式为AT_MOST(即wrap_content)或UNSPECIFIED(即0或未设置宽高)时,需要重写方法onMeasure方法并调用setMeasuredDimension方法手动设置宽高属性。
  3. 若含有子View时,且需要根据子View内容动态改变宽高时,需要先调用子View的measure方法测量后,再去获取其宽高并计算自身宽高。

onLayout

相比onMeasure方法而言,onLayout方法就显得简单多了,他的作用是告诉程序View的位置,但是这个位置默认不是由View本身决定的,而是由父控件调用其layout方法实现,在这我们可以看一下View的onLayout方法和layout方法

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
  
}


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

    ......
    
    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
    
        onLayout(changed, l, t, r, b)

    }

    ......

}

View本身的onLayout方法是一个空方法,真正确定布局位置的是layout方法。实际上,onLayout方法真正作用是用于确定子控件的位置信息,而其本身的位置是由父布局确定并调用layout进行设置。

所以如果是viewGroup时,便需要遍历所有的子View并调用其layout方法,从而来确定自己的布局位置。这里也参考LinearLayout的onLayout方法

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

    ......

    //遍历所有的子View
    for (int i = 0; i < count; i++) {

        final View child = getVirtualChildAt(i);

        ......

        //最终是调用child.layout方法去指定子View的位置
        setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                        childWidth, childHeight);

        ......

}

值得一提的时,用户可以通过调用layout的方式去调整本身的位置,这个技巧可以用于实现跟随手指滑动

onDraw()

最后就是View的内容绘制了,而实际上View的真正绘制是由draw方法控制的,在这个方法中依次调用了以下方法

  1. drawBackground(canvas) 绘制背景
  2. onDraw()  绘制该View内容,View的该方法为空方法,即提供给开发者重写完成绘制的
  3. dispatchDraw(canvas)这个方法可以阅读ViewGroup的源码,适用于绘制子布局
  4. onDrawForeground(canvas) 用于绘制装饰器,例如滚动条

以上方法中一般的自定义View只需要重写onDraw方法即可。

 

最后

以上就是健壮钢笔为你收集整理的Android自定义View——onMeasure,onLayout,onDraw的作用 View的绘制流程的全部内容,希望文章能够帮你解决Android自定义View——onMeasure,onLayout,onDraw的作用 View的绘制流程所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(55)

评论列表共有 0 条评论

立即
投稿
返回
顶部