Android UI繪製分析(二)-測量、佈局、繪製

本文源碼基於 Android sdk 26, 爲了邏輯清晰,省略了無關代碼,不排除後期從新加上相關代碼canvas

系統發送Message 繪製佈局, 啓動 requestLayout( )

系統發送消息開始繪製API調用流程圖以下:api

系統發送消息開始繪製API調用流程圖.jpg

這個要從應用程序啓動開始, 由於Android sdk使用Java寫的, 而Java程序運行是從main函數開始,因此咱們要先看 ActivityThread 。bash

public static void main(String[] args) {
        SamplingProfilerIntegration.start();
        CloseGuard.setEnabled(false);
        Environment.initForCurrentUser();
        Looper.prepareMainLooper();

        ActivityThread thread = new ActivityThread();
        thread.attach(false);

        if (sMainThreadHandler == null) {
            sMainThreadHandler = thread.getHandler();
        }
        Looper.loop();

        throw new RuntimeException("Main thread loop unexpectedly exited");
    }
複製代碼

該方法最後調用 Looper.loop() 表示主線程進入消息隊列循環,接下來的全部操做 須要 系統經過Binder機制主動向程序進程發送通知,子線程接受到信息後 向主線程發送Message,從而控制應用程序行爲方式。session

好比說當Activity顯示的時候,系統發送RESUME_ACTIVITY 消息,控制activity開始繪製。app

private class H extends Handler {
        ·····
        public void handleMessage(Message msg) {
            switch (msg.what) {
                 ······
                case RESUME_ACTIVITY:  
                    SomeArgs args = (SomeArgs) msg.obj;
                    handleResumeActivity((IBinder) args.arg1, true, args.argi1 != 0, true,args.argi3, "RESUME_ACTIVITY");
                    break;
                    ······
                }
        }
複製代碼

在handleResumeActivity()方法中,主要作兩件事: 第一,調用Activity的生命週期函數onResume(); 第二,將以前建立的DecorView添加到 ViewRootImp中,開始測量、佈局、繪製。less

final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
        // 該方法會調用到Activity的生命週期函數 onResume()
        r = performResumeActivity(token, clearHide, reason);

        if (r != null) {
            boolean willBeVisible = !a.mStartedActivity;
            if (r.window == null && !a.mFinished && willBeVisible) {
                r.window = r.activity.getWindow();
                View decor = r.window.getDecorView();
                decor.setVisibility(View.INVISIBLE);
                ViewManager wm = a.getWindowManager();
                WindowManager.LayoutParams l = r.window.getAttributes();
                a.mDecor = decor;
  
                if (a.mVisibleFromClient) {
                    if (!a.mWindowAdded) {
                        a.mWindowAdded = true;
                        //根據源碼能夠知道該方法的具體實如今WindowManagerImpl中
                        wm.addView(decor, l);
                    } 
                }
              ......
複製代碼

WindowManagerImpl中調用ide

//WindowManagerImpl中方法
    @Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        //mGlobal 即 WindowManagerGlobal
        mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
    }
複製代碼

WindowManagerGlobal 中調用函數

//WindowManagerGlobal 中方法
    public void addView(View view, ViewGroup.LayoutParams params,Display display, Window parentWindow) {
        ......
        synchronized (mLock) {
            root = new ViewRootImpl(view.getContext(), display);
            view.setLayoutParams(wparams);
            // do this last because it fires off messages to start doing things
            root.setView(view, wparams, panelParentView);
        }
    }
複製代碼

ViewRootImpl中調用oop

//ViewRootImpl中方法
    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
                ......
                requestLayout();
                ......
            }
        }
    }
複製代碼
@Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            scheduleTraversals();
        }
    }
複製代碼

通過一連串的調用,最後方法走到這裏,方法主要作一件事,發送一個Runnable 開始View的測量、佈局 、繪製,而後通知系統開始下一幀。佈局

void scheduleTraversals() {
        if (!mTraversalScheduled) {
           //mTraversalRunnable  中開始View的測量、佈局、繪製
            mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            //通知系統 繪製新一幀畫面, 即將View顯示到屏幕上
            notifyRendererOfFramePending();
        }
    }
複製代碼
final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }
複製代碼
void doTraversal() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
            performTraversals();
            if (mProfile) {
                Debug.stopMethodTracing();

                mProfile = false;
            }
        }
    }
複製代碼
private void performTraversals() {
    ......
    Rect frame = mWinFrame;
    ......
            
    // !!FIXME!! This next section handles the case where we did not get the
    // window size we asked for. We should avoid this by getting a maximum size from
    // the window session beforehand.
    if (mWidth != frame.width() || mHeight != frame.height()) {
       mWidth = frame.width();
       mHeight = frame.height();
    }
    ...... 
    //開始測量
    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    ......
     //佈局
    performLayout(lp, mWidth, mHeight);
    ......
    //繪製
    performDraw();
    ......    
複製代碼

測量

測量模式 MeasureSpec 介紹

程序使用一個32位 的 int型的整數 來表示view的尺寸信息, 其中該數據 高2位 表示測量模式, 低30位表示具體的大小數據。

EXACTLY: 精確模式,父控件 已經爲子控制肯定具體尺寸, 大小即爲低30位數值

AT_MOST : 最大值模式,子控件本身肯定本身的尺寸, 但不能超過父控件指定的最大值

UNSPECIFIED : 未肯定模式,父控件沒有作任何約束限制,子控件能夠本身指定任意大小

public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

        /** @hide */
        @IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
        @Retention(RetentionPolicy.SOURCE)
        public @interface MeasureSpecMode {}

        /**
         * Measure specification mode: The parent has not imposed any constraint
         * on the child. It can be whatever size it wants.
         */
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;

        /**
         * Measure specification mode: The parent has determined an exact size
         * for the child. The child is going to be given those bounds regardless
         * of how big it wants to be.
         */
        public static final int EXACTLY     = 1 << MODE_SHIFT;

        /**
         * Measure specification mode: The child can be as large as it wants up
         * to the specified size.
         */
        public static final int AT_MOST     = 2 << MODE_SHIFT;
        ......
}

複製代碼
父控件 確認子View的 MeasureSpec

開始測量子控件時, 父控件會根據自身的尺寸 和子控件的LayoutParam 肯定子控件的MeasureSepc

以下源碼可知,規則以下: 當前控件自身測量模式爲

  1. MeasureSpec.EXACTLY

    當子控件LayoutParams 爲 具體值:設置子控件 MeasureSpec.EXACTLY, 尺寸爲當前設置的尺寸

    當子控件LayoutParams 爲 MATCH_PARENT : 設置子控件 MeasureSpec.EXACTLY, 尺寸爲父控件尺寸

    當子控件LayoutParams 爲 WRAP_CONTENT:設置子控件MeasureSpec.AT_MOST,尺寸不超過父控件尺寸

  2. MeasureSpec.AT_MOST

    當子控件LayoutParams 爲 具體值:設置子控件 MeasureSpec.EXACTLY, 尺寸爲當前設置的尺寸

    當子控件LayoutParams 爲 MATCH_PARENTWRAP_CONTENT :設置子控件MeasureSpec.AT_MOST,尺寸不超過父控件尺寸

  3. MeasureSpec.UNSPECIFIED

    當子控件LayoutParams 爲 具體值:設置子控件 MeasureSpec.EXACTLY, 尺寸爲當前設置的尺寸

    當子控件LayoutParams 爲 MATCH_PARENTWRAP_CONTENT : 設置子控件 MeasureSpec.UNSPECIFIED,尺寸根據當前配置 設置爲0 或當前父控件尺寸

/**
     *
     * @param spec The requirements for this view
     * @param padding The padding of this view for the current dimension and
     *        margins, if applicable
     * @param childDimension How big the child wants to be in the current
     *        dimension
     * @return a MeasureSpec integer for the child
     */
    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be // bigger than us. resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; // Parent has imposed a maximum size on us case MeasureSpec.AT_MOST: if (childDimension >= 0) { // Child wants a specific size... so be it resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size, but our size is not fixed. // Constrain child to not be bigger than us. resultSize = size; resultMode = MeasureSpec.AT_MOST; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }
複製代碼
/**
     * View.MeasureSpec中的方法
     */
      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);
            }
        }
複製代碼
子view測量本身

經過父控件 爲本身指定的 MeasureSpec 和自身須要的尺寸, 計算出本身最後的大小。規則以下

/**
     *
     * @param size 自身須要的尺寸
     * @param measureSpec 父控件爲本身指定的MeasureSpec
     * @return  控件最後的尺寸
     */
    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:
            //使用xml中設置的尺寸,但最大值不超過父控件規定的尺寸
            result =  Math.min(size, specSize);
           break;
        case MeasureSpec.EXACTLY:
            //精確模式,直接使用具體的值
            result = specSize;
            break;
        }
        return result;
    }
複製代碼

佈局

經過ViewRootImpl中的performLayout(...)開始當前界面的佈局。

使用getValidLayoutRequesters(...)方法 獲得當前控件內全部須要進行佈局的子View, (過濾掉狀態爲View.Gone的view)

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,int desiredWindowHeight) {
        final View host = mView;
        ......
        host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());  
        ......
        ArrayList<View> validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters,false);
        int numValidRequests = validLayoutRequesters.size();
        for (int i = 0; i < numValidRequests; ++i) {
            final View view = validLayoutRequesters.get(i);
            view.requestLayout();
        }
      ......
    }
複製代碼

View.layout(...)方法 完成當前控件位置的設定,在該方法中會回調onLayout,繼承view的ViewGroup會重寫該方法,實現本身子控件 佈局的邏輯。

public void layout(int l, int t, int r, int b) {
        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }
        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;
        ......

        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);
            ......
        }
     ......
    }
複製代碼

好比如下代碼爲 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);
        }
    }
複製代碼

繪製

經過ViewRootImpl中的performDraw()開始當前界面的繪製。api調用流程圖以下:

繪製API調用流程圖.png

在View.draw()開始當前控件的繪製, 繪製步驟如註釋

  1. 繪製背景
  2. 繪製當前控件內容
  3. 繪製子控件
  4. 繪製前景,滾動條裝飾等
public void draw(Canvas canvas) {
       ......
        /*
         * 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
         *      4. Draw children
         *      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)
        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
            if (!dirtyOpaque) onDraw(canvas);

            // Step 4, draw the children
            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; } 複製代碼

View子類控件重寫 onDraw()方法 繪製自身, 如LineaLayout中:

@Override
    protected void onDraw(Canvas canvas) {
        if (mOrientation == VERTICAL) {
            drawDividersVertical(canvas);
        } else {
            drawDividersHorizontal(canvas);
        }
    }
複製代碼

viewGroup控件 重寫dispatchDraw()方法 繪製 子控件

@Override
    protected void dispatchDraw(Canvas canvas) {
       ......
       drawChild(canvas, child, drawingTime
       ......
    }
複製代碼
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        return child.draw(canvas, this, drawingTime);
    }
複製代碼

完~

(若有不足,歡迎指出,共同窗習,共同進步)

相關文章
相關標籤/搜索