自定义View从实现到原理(五)- View的工作流程(二)

自定义View从实现到原理(五)

接着上一篇博客说的,我们这次来梳理View的Measure,Layout以及Draw这三个流程。

View的Measure流程

我们已经知道了,measure就是用来测量View的宽和高的,不过可以分为View以及ViewGroup这两种情况,但是ViewGroup除了完成自身的测量之外,还要遍历的调用子元素的measure()方法,我们来依次分析一下:

View的measure流程

首先就来看一下View‘中的onMeasure()方法:

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

这个函数里面调用了另外一个函数setMeasuredDimension(),那就继续看下去:

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int opticalWidth  = insets.left + insets.right;
            int opticalHeight = insets.top  + insets.bottom;

            measuredWidth  += optical ? opticalWidth  : -opticalWidth;
            measuredHeight += optical ? opticalHeight : -opticalHeight;
        }
        setMeasuredDimensionRaw(measuredWidth, measuredHeight);
    }

可以看到设置了measuredWidth以及measuredHeight这两个值,很显然就是设置View的宽和高的,在onMeasure()方法中,函数setMeasuredDimension() 内通过两个 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;
    }

之前介绍过,specMode就是View的测量模式,specSize就是View的测量大小。这个函数可以清楚的看出,是根据不同的测量模式来返回不同的result值:

1.在AT_MOST,EXACTLY,两种模式下,返回的都是specSize的值,根据之前写过的,这两个模式一个代表的是wrap_content一个代表的是match_parent类型(或者是确定值),那么我们就可以知道,在xml中设置的不管是match_parent属性还是wrap_content属性,效果都会是一样的,都是match_parent类型的样式,所以如果我们设置的是wrap_content属性,就需要在View中对其进行处理,这也就是我们第一篇博客重写onMeasure()内容的原理;

2.在UNSPECIFIED测量模式下,返回的是size,也就是这个函数的第一个参数的值,回看onMeasure()方法中的这个参数,调用的是getSuggestedMinimumWidth()方法以及getSuggestedMinimumHeight()这两个方法,不过本质上是一样的,我们就选定第一个来看看:

protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }

很简单的一行代码,如果View没有设置背景,那么取值为mMinWidth,这个mMinWidth是可以设置的,对应的值是Android:minWidth这个属性设置的值或者是View中的setMinimumWidth()方法的值,如果不设置的话默认就会是0,简单看一下这个setMinimumWidth()方法:

public void setMinimumWidth(int minWidth) {
        mMinWidth = minWidth;
        requestLayout();
    }

很清晰的代码,根据传入的参数设置了mMinWidth的值,我们回头接着看之前的代码:

当View设置背景的时候,取值为max(mMinWidth, mBackground.getMinimumWidth()),也就是()内mMinWidth与mBackground.getMinimumWidth()中的最大值, mMinWidth我们已经看过了,那么就来看看这个mBackground.getMinimumWidth(),点击进去之后我们可以看到,mBackground是Drawable类型的,getMinimumWidth()方法如下:

 public int getMinimumWidth() {
        final int intrinsicWidth = getIntrinsicWidth();
        return intrinsicWidth > 0 ? intrinsicWidth : 0;
    }

变量intrinsicWidth得到的是这个Drawable也就是View背景的固有宽度,如果固有宽度大于0那就返回这个值要不然就返回0,那么总体来说就是,当自定义View的测量模式是UNSPECIFIED时,如果没有设置背景就返回mMinWidth,设置了背景就返回mMinWidth和背景的固有宽度的最大值。

ViewGroup的measure流程

这个暂时先留着吧,目前我还用不到自定义ViewGroup这个知识点,等以后用到了再开一篇博客,不好意思兄弟们。

View的Layout流程

冲冲冲下一部分开始,这部分就是View的Layout流程。layout方法的作用呢,就是确定元素的位置,在ViewGroup中用layout来确定子元素的位置,在View中用layou来确定自身的位置,接下来就直接看一下View中的layout方法:

 public void layout(int l, int t, int r, int b) {
 	****
		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);
	****
		
}

总结来看重点就是这一部分,首先看传入的四个参数,l,t,r,b,这四个参数,分别代表了View从左,上,右,下相对于父容器的距离,在接下来就定义了changed,查看一下setFrame()方法中做了什么:

protected boolean setFrame(int left, int top, int right, int bottom) {
	****
            mLeft = left;
            mTop = top;
            mRight = right;
            mBottom = bottom;
       ****
}

这部分重点也就这些,在layout()方法中的参数传入到了setFrame()方法中,在这个里面将mLeft,mTop,mRight,mButtom这四个值初始化,这样就确定了View在父容器中的位置,在这个方法之后,layout()又会继续进行操作,调用onLayout()函数方法:

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

这是个空方法,和onMeasure()方法类似,在确定位置时根据不同的控件有不同的实现。那么这一部分到这里基本已经梳理的差不多了,下一部分,开冲。

View的Draw流程

这一部分就是View的绘制过程了,官方注释中很清楚的说明了每一步的做法,分别是:
1.如果需要,则绘制背景;
2.保存当前canvas层;
3.绘制View的内容;
4.绘制子View;
5.如果需要,绘制View的褪色边缘,类似于阴影效果;
6.绘制装饰,比如滚动条;
对于自定义View,我们需要仔细梳理的是1,3,6,第四步是自定义ViewGroup实现,
第二步第五步可以跳过,不做分析,我们来看其他几部分:

绘制背景

绘制背景调用了View的drawBackground()方法:

private void drawBackground(Canvas canvas) {
        final Drawable background = mBackground;
        if (background == null) {
            return;
        }

        setBackgroundBounds();

        // Attempt to use a display list if requested.
        if (canvas.isHardwareAccelerated() && mAttachInfo != null
                && mAttachInfo.mThreadedRenderer != null) {
            mBackgroundRenderNode = getDrawableRenderNode(background, mBackgroundRenderNode);

            final RenderNode renderNode = mBackgroundRenderNode;
            if (renderNode != null && renderNode.hasDisplayList()) {
                setBackgroundRenderNodeProperties(renderNode);
                ((RecordingCanvas) canvas).drawRenderNode(renderNode);
                return;
            }
        }

        final int scrollX = mScrollX;
        final int scrollY = mScrollY;
        if ((scrollX | scrollY) == 0) { //1
            background.draw(canvas);
        } else {
            canvas.translate(scrollX, scrollY);
            background.draw(canvas);
            canvas.translate(-scrollX, -scrollY);
        }
    }

在1处,我们可以看到对scrollx以及scrolly进行了判断处理,如果滑动了的话就会在偏移后的canvas绘制背景,canvas就是画布;

绘制View的内容

绘制View内容自然就是我们熟悉的onDraw()方法了,和之前一样,这个方法也是一个空实现,因为不同的View实现的内容都不一样,我们需要在自定义的View中重写此方法:

protected void onDraw(Canvas canvas) {
    }

绘制装饰

这一部分调用的是View方法的onDrawForeground()方法:

public void onDrawForeground(Canvas canvas) {
        onDrawScrollIndicators(canvas);
        onDrawScrollBars(canvas);

        final Drawable foreground = mForegroundInfo != null ? mForegroundInfo.mDrawable : null;
        if (foreground != null) {
            if (mForegroundInfo.mBoundsChanged) {
                mForegroundInfo.mBoundsChanged = false;
                final Rect selfBounds = mForegroundInfo.mSelfBounds;
                final Rect overlayBounds = mForegroundInfo.mOverlayBounds;

                if (mForegroundInfo.mInsidePadding) {
                    selfBounds.set(0, 0, getWidth(), getHeight());
                } else {
                    selfBounds.set(getPaddingLeft(), getPaddingTop(),
                            getWidth() - getPaddingRight(), getHeight() - getPaddingBottom());
                }

                final int ld = getLayoutDirection();
                Gravity.apply(mForegroundInfo.mGravity, foreground.getIntrinsicWidth(),
                        foreground.getIntrinsicHeight(), selfBounds, overlayBounds, ld);
                foreground.setBounds(overlayBounds);
            }

            foreground.draw(canvas);
        }
    }

在一开始我们就可以看出来了,这个方法用于绘制ScrollBar等装饰,并将它们绘制在视图内容的上层。

好了,到这里我们View的源码分析部分基本上已经全部搞定了,之前我们实现过一个简单的自定义View,现在看来过于简单,经过了这段时间的源码阅读与分析,我们已经有能力做一些复杂的自定义View了,下一篇博客我们就会进行自定义组合控件,来完善巩固我们学的知识,快休息休息!!

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值