天天看點

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的繪制流程”這樣的标題

繼續閱讀