欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Android学习之View绘制

程序员文章站 2022-07-12 20:16:44
...

安卓APP中最先让用户注意到的无疑是APP的界面,一个界面好与不好,很大程度上影响到用户的体验。因此,作为一个安卓开发人员,学习与了解View界面的绘制就十分重要了。在我们平常的开发过程中,经常都使用到View相关知识,任何布局和控件,比如TextView、Button、ImageView等等,都是直接或者间接的继承View,这些都是系统提供的,可能我们使用过程中对View绘制的流程也就没什么感觉。在Android中,View绘制过程最主要的三个过程,分别是onMeasure()、onLayout()和onDraw()。下面就这三点详细学习一下:

一. onMeasure

onMeasure,顾名思义,测量的意思,方法的主要作用是测量视图的大小,以下是谷歌官方API文档下对这个方法介绍的截图

Android学习之View绘制

可以看到,上面截图里面提到,onMeasure这个方法是在measure这个方法里面有被调用,因此我们打开measure这个方法的源码进行分析

/**
     * <p>
     * This is called to find out how big a view should be. The parent
     * supplies constraint information in the width and height parameters.
     * </p>
     *
     * <p>
     * The actual measurement work of a view is performed in
     * {@link #onMeasure(int, int)}, called by this method. Therefore, only
     * {@link #onMeasure(int, int)} can and must be overridden by subclasses.
     * </p>
     *
     *
     * @param widthMeasureSpec Horizontal space requirements as imposed by the
     *        parent
     * @param heightMeasureSpec Vertical space requirements as imposed by the
     *        parent
     *
     * @see #onMeasure(int, int)
     */
    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int oWidth  = insets.left + insets.right;
            int oHeight = insets.top  + insets.bottom;
            widthMeasureSpec  = MeasureSpec.adjust(widthMeasureSpec,  optical ? -oWidth  : oWidth);
            heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
        }

        // Suppress sign extension for the low bytes
        long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
        if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);

        final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;

        // Optimize layout by avoiding an extra EXACTLY pass when the view is
        // already measured as the correct size. In API 23 and below, this
        // extra pass is required to make LinearLayout re-distribute weight.
        final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
                || heightMeasureSpec != mOldHeightMeasureSpec;
        final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
                && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
        final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
                && getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
        final boolean needsLayout = specChanged
                && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);

        if (forceLayout || needsLayout) {
            // first clears the measured dimension flag
            mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;

            resolveRtlPropertiesIfNeeded();

            int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                // measure ourselves, this should set the measured dimension flag back
                onMeasure(widthMeasureSpec, heightMeasureSpec);
                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            } else {
                long value = mMeasureCache.valueAt(cacheIndex);
                // Casting a long to int drops the high 32 bits, no mask needed
                setMeasuredDimensionRaw((int) (value >> 32), (int) value);
                mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            }

            // flag not set, setMeasuredDimension() was not invoked, we raise
            // an exception to warn the developer
            if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
                throw new IllegalStateException("View with id " + getId() + ": "
                        + getClass().getName() + "#onMeasure() did not set the"
                        + " measured dimension by calling"
                        + " setMeasuredDimension()");
            }

            mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
        }

        mOldWidthMeasureSpec = widthMeasureSpec;
        mOldHeightMeasureSpec = heightMeasureSpec;

        mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
                (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
    }

这段代码有点长,不过关注一下重点部分代码就好了,首先,measure这个方法是final修饰的,说明不允许去重写这个方法。然后在这个方法里面,我们看到了onMeasure这个方法,并且上面的注释说measure ourselves, this should set the measured dimension flag back,大致意思是测量自身,设置测量尺寸的标志。接下来再看一下onMeasure的具体实现:

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

可以看到onMeasure方法的代码比较少,通过getDefaultSize来获取大小之后,然后通setMeasureDimension来设置大小,视图的大小最终是通过这个方法来设置的。这样一次measure过程就结束了。按照着这个流程,首先onMeasure方法是可以进行重写的,假设你想要重写了onMeasure方法,并在里面设置setMeasuredDimension(200, 200);,这样子无论你在布局中定义视图的大小是多少,最终显示在界面上的视图大小都是200*200。

以上是比较单一视图情况下的测量过程,经常看到一个视图下往往会有多个子视图,这时候就需要进行多一步的操作了,因此涉及到多次的measure过程。

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

然后for循环里面对每一个子视图进行measureChild操作,再来看看measureChild的实现,可以看到里面同样是调用了measure方法,接下来的流程就和上面的一样了。

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

二. onLayout

同样,onLayout,顾名思义,就是给一个视图设置布局的意思。当你视图大小测量完之后,接下来就是给视图安排布局,同样看看谷歌官方对于这个方法的介绍。

Android学习之View绘制

首先看一下View里面的onLayout方法,可以看到,方法里面并没有任何的实现方式,再来看看ViewGroup里面的onLayout方法,ViewGroup是继承View的,而LinearLayout、RelativeLayout等布局是继承ViewGroup的

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

ViewGroup里重写了这个方法,变成了一个抽象的方法,那么继承ViewGroup的子类都必须去实现这个方法,比如打开Linearlayout的源码,就可以看到具体的实现方式了

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

至于直接继承自View的基本控件,比如Button,ImageView等,打开源码可以看到重写实现了onLayout方法。由于源码过长,就不一一贴出源码了。

最后需要注意一个问题,那就是getMeasureWidth、getMeasureHeight和getWidth、getHeight之间的区别,首先第一点是,getMeasureWidth、getMeasureHeight是在onMeasure方法结束后才能调用的方法,而getWidth、getHeight是在onLayout方法完成后才能调用的方法,这是第一点区别;第二点区别是,getMeasureWidth、getMeasureHeight方法中的值是通过setMeasuredDimension()方法来进行设置的,而getWidth、getHeight方法中的值则是通过视图右边的坐标减去左边的坐标、下边的坐标减去上边的坐标计算出来的。因此,如果自定义布局的开发者不规范或者有特殊需求,有可能会导致两者返回的结果不一致。这也是会经常困惑到我们初学者的一个问题。

三. onDraw

在测量onMeasure和布局onLayout之后,接下来就到了比较核心的一步,也就是绘制onDraw。首先,了解官方文档是如何介绍这个方法的。相对于其他两个方法,官方文档这边对这个方法的介绍很简洁。

Android学习之View绘制

接下来View里面的draw这个方法,里面有很大一段注释说明了绘制的流程

Android学习之View绘制

从步骤流程上面来看,目前需要了解的最重要的一步就是第三步——绘制视图的内容,第三步里面实现了什么。

Android学习之View绘制

只有简单地判断之后,调用onDraw(Canvas canvas),但是onDraw方法里面是空的,这也就说明了对视图内容的绘制需要交给具体的子类去实现。通过上面的流程,梳理一下思路,大概就是一般来说View不会去帮我们实现视图内容的绘制,所有的想要实现的内容就需要到相对的具体子类中去实现,同时绘制是借助Canvas这个类去实现的,因此作为参数传入onDraw方法中。

流程图来表示这整个View绘制的大致流程:

Android学习之View绘制