Android View 繪制流程

一 開始的開始

相信很多小伙伴刻获,在去面試的時候郎仆,都會遇到面試官一臉正經的問你只祠,簡單的說下View的繪制流程。其實一遇到這個問題扰肌,我是奔潰的抛寝,因為完全無法講出第一句話,很多人都是一句曙旭,哦盗舰,view的繪制流程就是onMeasure()->onLayout()->onDraw()。onMeasure()方法就是計算View的大小桂躏,onLayout()就是計算view所在的位置钻趋,onDraw()就是開始繪制。balabala剂习,這個時候蛮位,面試官就會面帶微笑,說了句:哦鳞绕,然后呢失仁?

二 View繪制的開始

view的繪制,是從viewRootImp的performTraversals()開始的们何,performTraversal()的源碼很長萄焦,其實,我們只要知道以下的三個流程即可:

private void performTraversals() {

  performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
  performLayout(lp, mWidth, mHeight);
  performDraw();
}

好了冤竹,我們接下來來看看performMeasur(childWidthMeasureSpec,childHeightMeasureSpec)的代碼

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
    if (mView == null) {
        return;
    }
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
    try {
        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
}

從這里我們可以看出拂封,ViewRootImp中的performMeasure方法,最終是回調到View中的measure()方法中贴见。
我們接下來看performLayout方法

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
        int desiredWindowHeight) {
    mLayoutRequested = false;
    mScrollMayChange = true;
    mInLayout = true;

    final View host = mView;
    if (host == null) {
        return;
    }
    ....
    host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
    ....
    mInLayout = false;
}

在這段代碼中烘苹,我們省去了絕大部分頂層view的layout操作,只保留了繪制流程相關的代碼片部。從這段代碼我們可以看出performLayout最終會調用到view的layout方法镣衡,并把父view的left top right bottom傳給view中。至于view的layout方法是怎樣的档悠,我們后面再講廊鸥。
接下來,我們來看performDraw方法

private void performDraw() {
    if (mAttachInfo.mDisplayState == Display.STATE_OFF && !mReportNextDraw) {
        return;
    } else if (mView == null) {
        return;
    }

    final boolean fullRedrawNeeded = mFullRedrawNeeded;
    mFullRedrawNeeded = false;

    mIsDrawing = true;
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "draw");
    try {
        draw(fullRedrawNeeded);
    } finally {
        mIsDrawing = false;
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
}

我們主要看draw()方法辖所,這個方法最后其實調用的就是

public final void dispatchOnDraw() {
    if (mOnDrawListeners != null) {
        mInDispatchOnDraw = true;
        final ArrayList<OnDrawListener> listeners = mOnDrawListeners;
        int numListeners = listeners.size();
        for (int i = 0; i < numListeners; ++i) {
            listeners.get(i).onDraw();
        }
        mInDispatchOnDraw = false;
    }
}

這個方法是用來干嘛的惰说?其實就是通知已經注冊了OnDrawListener的監(jiān)聽者,view已經要開始繪制了缘回。其實吆视,也就是到了onDraw()方法中了典挑。
通過源碼,一步一步追蹤啦吧,我們可以發(fā)現您觉,view的這個繪制流程其實是這樣的
ViewRootImp->PerformTrasvel()->PerformMeasure()->measure()->onMeasure()
->performLayout()->onLayout()
->performDraw()->draw()->dispatchDraw()->onDraw()

三 View的measure流程

1 View的measure(int widthMeasureSpec, int heightMeasureSpec)

我們通過看注釋,知道了measure的作用是授滓,計算這個view應該是多大琳水,因為父布局提供了傳入制定的寬高參數。具體的測量工作其實是放在onMeasure(int般堆,int)方法中去執(zhí)行的在孝,onMeasure(int,int)方法能夠也必須被繼承(當我們重寫view)的時候淮摔。
好了私沮,那么問題來了,什么是widthMeasureSpec 和heighMeasureSpec噩咪?
我們回到ViewRootImp的performTrasvel()方法中

childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
            childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

也即是顾彰,width和heigh的MeasureSpec是通過getRootMeasureSpec(int,int)方法中獲取的,

private static int getRootMeasureSpec(int windowSize, int rootDimension) {
    int measureSpec;
    switch (rootDimension) {

    case ViewGroup.LayoutParams.MATCH_PARENT:
        // Window can't resize. Force root view to be windowSize.
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
        break;
    case ViewGroup.LayoutParams.WRAP_CONTENT:
        // Window can resize. Set max size for root view.
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
        break;
    default:
        // Window wants to be an exact size. Force root view to be that size.
        measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
        break;
    }
    return measureSpec;
}

核心的計算方法就是MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);這個方法胃碾。
其實這個方法的作用就是涨享,MeasureSpec通過SpecMode和SpecSize來判斷一個view應該是多大。那么仆百,什么是specMode厕隧?
SepcMode一共有三類,除了我們上面看到的MeasureSpec.EXACTLY外俄周,還有MeasureSpec.UNSPECIFIED和MeasureSpec.AT_MOST
MeasureSpec.UNSPECIFIED: 父布局沒有指定任何的約束大小吁讨,子布局想多大就多大;
MeasureSpec.EXACTLY: 父布局已經指定了一個確切的大小峦朗,子布局的大小就是SpecMode的大薪ㄉァ;
MeasureSpec.AT_MOST: 子布局可以想多大就多大波势,但是不能超過SpecSize的值

2 View的onMeasure(int widthMeasureSpec, int heightMeasureSpec)

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

這么簡單的幾行代碼翎朱,其實暗藏了三個重要的方法,我們從內到外進行剖析:

1 getSuggestedMinimumWidth()

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

這個方法的作用尺铣,就是獲取這個view被建議的最小的寬度拴曲。如果這個view被我們設置了background,那么凛忿,就取最小寬度和background中最小寬度兩者之間最大的那一個數字澈灼。獲取getSuggestedMinimumHeight()方法也是同理,這里不單獨在做分析
2 getDefaultSize(int size, int measureSpec)
我們下面來看一下源碼:

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

這段代碼也很簡單,首先view會去獲取父view傳進來的measureSpec叁熔,獲取specSize和specMode兩個值委乌。當當前的specMode是MeasureSpec.UNSPECIFIED,則返回我們上一點分析的者疤,minimumWidth的長度福澡,如果是 MeasureSpec.AT_MOST和MeasureSpec.EXACTLY,那么其實就是specSize的大小驹马。

3 ViewGroup的measure流程###

ViewGroup其實沒有onMeasure()方法,ViewGroup的measure流程除秀,最后就是走的measureChildren(int,int)方法中糯累,我們接下來看這個方法

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

measureChildren的思想就是,取出一個個子元素册踩,然后過去layoutParams泳姐,然后創(chuàng)建children的MeasureSpec,并把這個spec發(fā)給child暂吉,讓child去回調measure方法胖秒,這就回到了我們第一點開始分析的內容了。

三 View的Layout流程

view的layout流程慕的,簡單概括阎肝,就是,指定一個view的位置和它的真正的大小肮街。這就是我們在上面所說的风题,measure過程并不能真正得到view的實際大小,要在layout的過程中才可以被確定嫉父。好了沛硅,接下來繼續(xù)看源碼:

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;

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

        if (shouldDrawRoundScrollbar()) {
            if(mRoundScrollbarRenderer == null) {
                mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
            }
        } else {
            mRoundScrollbarRenderer = null;
        }

        mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnLayoutChangeListeners != null) {
            ArrayList<OnLayoutChangeListener> listenersCopy =
                    (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
            int numListeners = listenersCopy.size();
            for (int i = 0; i < numListeners; ++i) {
                listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
            }
        }
    }

    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的流程就是,通過setOpticalFrame或者setFrame方法绕辖,穿進去l摇肌,t,r仪际,b围小,這樣我們的view的位置就可以確定了。接下來就回調onLayout()方法弟头,由子view去決定當前的layout要怎么控制吩抓。

三 View的draw流程

draw流程比較簡單,這里不大篇幅的上代碼赴恨,根據注釋疹娶,我們可以得到view的draw流程主要是有5步:
1 畫背景
2 如果有需要的話,保存當前畫布
3 畫view自己的內容
4 如果有子View伦连,畫出子view
5 畫一些裝飾(decorations)view雨饺,比如前景色钳垮、srcollbars

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市额港,隨后出現的幾起案子饺窿,更是在濱河造成了極大的恐慌,老刑警劉巖移斩,帶你破解...
    沈念sama閱讀 212,686評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件肚医,死亡現場離奇詭異,居然都是意外死亡向瓷,警方通過查閱死者的電腦和手機肠套,發(fā)現死者居然都...
    沈念sama閱讀 90,668評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來猖任,“玉大人你稚,你說我怎么就攤上這事≈焯桑” “怎么了刁赖?”我有些...
    開封第一講書人閱讀 158,160評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長长搀。 經常有香客問我宇弛,道長,這世上最難降的妖魔是什么盈滴? 我笑而不...
    開封第一講書人閱讀 56,736評論 1 284
  • 正文 為了忘掉前任涯肩,我火速辦了婚禮,結果婚禮上巢钓,老公的妹妹穿的比我還像新娘病苗。我一直安慰自己,他們只是感情好症汹,可當我...
    茶點故事閱讀 65,847評論 6 386
  • 文/花漫 我一把揭開白布硫朦。 她就那樣靜靜地躺著,像睡著了一般背镇。 火紅的嫁衣襯著肌膚如雪咬展。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 50,043評論 1 291
  • 那天瞒斩,我揣著相機與錄音破婆,去河邊找鬼。 笑死胸囱,一個胖子當著我的面吹牛祷舀,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 39,129評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼裳扯,長吁一口氣:“原來是場噩夢啊……” “哼抛丽!你這毒婦竟也來了?” 一聲冷哼從身側響起饰豺,我...
    開封第一講書人閱讀 37,872評論 0 268
  • 序言:老撾萬榮一對情侶失蹤亿鲜,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后冤吨,有當地人在樹林里發(fā)現了一具尸體蒿柳,經...
    沈念sama閱讀 44,318評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,645評論 2 327
  • 正文 我和宋清朗相戀三年锅很,在試婚紗的時候發(fā)現自己被綠了其馏。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,777評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡爆安,死狀恐怖,靈堂內的尸體忽然破棺而出仔引,到底是詐尸還是另有隱情扔仓,我是刑警寧澤,帶...
    沈念sama閱讀 34,470評論 4 333
  • 正文 年R本政府宣布咖耘,位于F島的核電站翘簇,受9級特大地震影響,放射性物質發(fā)生泄漏儿倒。R本人自食惡果不足惜版保,卻給世界環(huán)境...
    茶點故事閱讀 40,126評論 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望夫否。 院中可真熱鬧彻犁,春花似錦、人聲如沸凰慈。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,861評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽微谓。三九已至森篷,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間豺型,已是汗流浹背仲智。 一陣腳步聲響...
    開封第一講書人閱讀 32,095評論 1 267
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留姻氨,地道東北人钓辆。 一個月前我還...
    沈念sama閱讀 46,589評論 2 362
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親岩馍。 傳聞我的和親對象是個殘疾皇子碉咆,可洞房花燭夜當晚...
    茶點故事閱讀 43,687評論 2 351

推薦閱讀更多精彩內容