天天看点

Android源码探索-View体系(一)

View简介

View体系是整个Android中很大分量的一部分,也是最直观的体现(用户界面控件).View是整个View体系中所有控件的基类,其子类控件包括有ViewGroup、TextureView、SurfaceView、ImageView等.

安卓中的坐标系

说到View,不得不说一下Android的坐标系

1.屏幕坐标系

屏幕坐标系是以手机屏幕的左上角顶点为原点(0,0),向右为x轴正方向,向下为y轴正方向的坐标系

Android源码探索-View体系(一)

以下的两个获取坐标的方法就是基于屏幕坐标系的

getRawX();       //获取子View左上角距离屏幕X轴的距离
  getRawY();       //获取子View左上角距离屏幕Y轴的距离
           

2.View的坐标系

我们知道,ViewGroup是可以多级嵌套使用的,也就是说,一个控件可以最外面是一个ViewGroup  A,A中呢又有一层ViewGroup B,而B中有一个View C.那么C的坐标系呢,就是相对于其父控件ViewGroup B的.

Android源码探索-View体系(一)

图画的有点丑,但是应该还算清晰(如果有好用的画图软件的话,劳驾推荐一波).

以下获取坐标值的方法就是基于View的坐标系的,那么在上图这个例子中,就是子控件C相对于父控件B的坐标系的坐标值的获取方法

getLeft();     //ViewC的左上角相对父控件B的x轴的距离
getRight();    //ViewC的右下角相对父控件B的x轴的距离
getTop();    //ViewC的左上角相对父控件B的y轴的距离
getBottom();    //ViewC的右下角相对父控件B的y轴的距离
           

好了,基础的简介就到这里,我们开始进入源码分析

首先呢,我们看View的声明:

public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource 
           

先看看Drawable.Callback

public interface Callback {
        void invalidateDrawable(@NonNull Drawable who);//当drawable需要重绘时执行,这个点的drawable将不可用(或者最少一部分不可用)
        void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when);//drawable通过调用该方法来调度下一帧的动画
        void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what);//可以取消之前通过scheduleDrawable方法调度的某一帧
    }
           

三个方法都是drawable的动画的调度方法

再看KeyEvent.Callback

public interface Callback {
        boolean onKeyDown(int keyCode, KeyEvent event);
        boolean onKeyLongPress(int keyCode, KeyEvent event);
        boolean onKeyUp(int keyCode, KeyEvent event);
        boolean onKeyMultiple(int keyCode, int count, KeyEvent event);
    }
           

KeyEvent.Callback中的前三个方法我们比较常见,分别是Down,Up,LongPress触摸事件的回调,而onKeyMultiple()方法是在快速多次触发down/up事件的情况下调用

最后AccessibilityEventSource

public interface AccessibilityEventSource {
    public void sendAccessibilityEvent(int eventType);
    public void sendAccessibilityEventUnchecked(AccessibilityEvent event);
}
           

这个接口是和辅助功能相关的,在这里我们先不管它

View的绘制过程

view能够精准的展示到手机屏幕上,是需要经过精准的计算和绘制的.其绘制过程会经历三个步骤:Measure,Layout,Draw.整个View的绘制是从ViewRootImpl类的performsTraversals()方法开始的,有兴趣的朋友可以下载源码自己看一下。

Measure

我们先来看measure,和measure相关的有三个方法:

public final void measure(int widthMeasureSpec, int heightMeasureSpec);
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec);
private void  setMeasureDimension(int measuredWidth, int measuredHeight);
           

从函数声明来看,measure()被final修饰符修饰,无法被子类重写;onMeasure()可以被子类重写,我们写的自定义View通过重写该方法重新获取View的测量值,如果不重写的话,获取的是默认的测量值通过setMeasureDimension()方法保存测量值.最后一个方法是保存测量值的

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        ...
        final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;//强制计算
        ...
        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;
            }

            ...
    }
           

measure()方法里的代码并不算特别多,前面的一些代码是配置获取的代码省略掉,来看最关键的逻辑,如果强制计算或需要计算,将执行onMeasure方法

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
           
public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);//获取view的测量模式
        int specSize = MeasureSpec.getSize(measureSpec);//获取view的测量大小

        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }
           

这里出现了specMode,那我们就简单介绍一下,MeasureSpec类中MeasureSpecMode有三种

public static final int UNSPECIFIED ;//未指定子view的大小,子view可以为任意大小
public static final int EXACTLY;//子view最多是父view指定的大小
public static final int AT_MOST;//父view指定了子view的大小
           

getDefaultSize方法中根据测量模式,返回不同的size大小,当如果是UNSPECIFIED时,返回的是系统建议的最小宽高;如果是AT_MOST和EXACTLY,则返回的测量大小specSize.

所以measure的整个流程就是首先measure()获取保存的参数,再根据设置来判断是否强制测量或忽略之前的计算结果并重新测量,onMeasure()方法则是实际测量的操作,可以被子类重写,默认实现是根据测量模式获取测量大小或默认最小宽高值并保存.

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

            ...

        }

        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;

        if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
            mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
            notifyEnterOrExitForAutoFillIfNeeded(true);
        }
    }
           

看来layout()和前面的measure()类似,根据布局是否发生改变或强制重新计算来判断是否执行onLayout()方法,我们继续看onLayout()方法

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

看到是空实现的,那么如果子类没有重写onLayout()方法呢?难道不执行布局了吗?我们往回看,发现在调用onLayout之前有根据是否是光学布局模式来调用setOpticalFrame()和setFrame()

private boolean setOpticalFrame(int left, int top, int right, int bottom) {
        Insets parentInsets = mParent instanceof View ?
                ((View) mParent).getOpticalInsets() : Insets.NONE;
        Insets childInsets = getOpticalInsets();
        return setFrame(
                left   + parentInsets.left - childInsets.left,
                top    + parentInsets.top  - childInsets.top,
                right  + parentInsets.left + childInsets.right,
                bottom + parentInsets.top  + childInsets.bottom);
    }
           

setOpticalFrame()内部调用的也是setFrame(),我们继续来看setFrame()

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;
            mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
            ...
          
        }
        return changed;
    }
           

mRenderNode.setLeftTopRightBottom(mLeft,mTop,mRight,mBottom)方法,实质上已经将四个参数保存下来了,记录了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 绘制view内容
         *      4. Draw children 绘制子view
         *      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) 一般情况下省略第2-5步
        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 绘制view内容
            if (!dirtyOpaque) onDraw(canvas);

            // Step 4, draw the children 绘制子view
            dispatchDraw(canvas);

            drawAutofilledHighlight(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);

            // Step 7, draw the default focus highlight 绘制默认焦点高亮
            drawDefaultFocusHighlight(canvas);

            if (debugDraw()) {
                debugDrawFocus(canvas);
            }

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

       ...
    }
           

draw(Canvas canvas)方法比较长,大概120行左右,这里只剪取了其中的关键代码,从源代码上来看,绘制流程已经很清楚了,需要注意的是,绘制本身是调用onDraw()方法,绘制子view是调用dispatchDraw()方法.

单个view的绘制流程就是以上,ViewGroup的话用的比较多,而且种类很多,也比较复杂,之后应该会根据单独的类来一个个分析,比如“LinearLayout的绘制流程”这样的标题

继续阅读