android登錄狀態改變,android自定義view流程,Android 自定義View--從源碼理解View的繪制流程

 2023-10-05 阅读 24 评论 0

摘要:前言在Android的世界里,View扮演著很重要的角色,它是Android世界在視覺上的具體呈現。Android系統本身也提供了很多種原生控件供我們使用,然而在日常的開發中我們很多時候需要去實現一些原生控件無法實現的效果。這個時候,我們就不得不采取自定義

前言

在Android的世界里,View扮演著很重要的角色,它是Android世界在視覺上的具體呈現。Android系統本身也提供了很多種原生控件供我們使用,然而在日常的開發中我們很多時候需要去實現一些原生控件無法實現的效果。這個時候,我們就不得不采取自定義View的方式來實現我們所需要的效果。其實要想使用自定義View,首先我們應該對View的繪制流程有一個基本的了解,只有掌握了View的繪制原理,才能更好的掌握自定義View這一技能。

走進源碼

1.起點--performTraversals():

任何一件事都有一個事件的起點,View的繪制流程同樣如此,它的起點就在ViewRootImpl的performTraversals方法。在這個方法中,會依次調用到三個重要的方法,它們分別是performMeasure、performLayout、performDraw方法;而這三個方法的內部又分別會調用到View的measure、layout、draw方法。由于performTraversals方法源碼比較長,這里只貼出其內部最終調用到View的measure、layout、draw方法的關鍵代碼行:

android登錄狀態改變?private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {

if (mView == null) {

return;

}

Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");

try {

android怎樣自定義類?mView.measure(childWidthMeasureSpec, childHeightMeasureSpec); // View的measure方法

} finally {

Trace.traceEnd(Trace.TRACE_TAG_VIEW);

}

}

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, int desiredWindowHeight) {

android layout、........

final View host = mView;

if (host == null) {

return;

}

........

自定義view流程,Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout");

try {

host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight()); // View的layout方法

........

} finally {

Trace.traceEnd(Trace.TRACE_TAG_VIEW);

android五種布局,}

........

}

private void performDraw() {

if (mAttachInfo.mDisplayState == Display.STATE_OFF && !mReportNextDraw) {

return;

android自定義控件。} else if (mView == null) {

return;

}

........

Trace.traceBegin(Trace.TRACE_TAG_VIEW, "draw");

........

android,try {

boolean canUseAsync = draw(fullRedrawNeeded); // draw方法內部調用drawSoftware方法

........

}

........

}

安卓自定義view,private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,

boolean scalingRequired, Rect dirty, Rect surfaceInsets) {

// Draw with software renderer.

final Canvas canvas;

........

try {

android 自定義view繪制流程、canvas = mSurface.lockCanvas(dirty);

........

mView.draw(canvas); // View的draw方法

........

}

........

android自定義view流程?return true;

}

現在,我們對View繪制的流程有了一個簡單的了解,它要經歷measure、layout、draw這三個流程才能最終被繪制到屏幕上。其中,measure完成對View的測量,layout完成對View的布局,draw完成對View的繪制。

2.MeasureSpec:

在開始閱讀View繪制的三個流程之前,我們還需要知道MeasureSpec這個類的作用。它是View內部的一個類,這個類封裝了從父布局傳遞到子View的布局要求,即對View寬度和高度的要求,它由模式和尺寸兩部分組成,有三種可能的模式:

2.1.EXACTLY:

安卓view的繪制流程。精確值模式,父布局已經確定了子View所需的大小。對應于我們在布局中給View的寬高指定了具體的數值或設置成match_parent。

2.2.AT_MOST:

最大值模式,在指定的大小范圍內(由父布局決定),子元素可以任意增大。對應于我們在布局中給View的寬高設置成wrap_content。

2.3.UNSPECIFIED:

未指定模式,父布局沒有對子View施加任何約束,它可以是任意大小。這種我們日常開發基本上沒用到過。

之所以說到這個類,是因為在View的measure方法中,接收兩個32位的int類型的參數。而這兩個參數均是通過MeasureSpec獲取到的,它的高兩位代表測量模式,低30位代表測量尺寸。而關于這兩個參數值的確定,還要根據View的布局參數(即LayoutParams)和父布局的約束而共同決定,這一點在MeasureSpec類的源碼解釋中也能體現。

android自定義view面試,3.測量--onMeasure():

現在開始閱讀View繪制的第一個流程--測量。前面講到的performTraversals方法中,最終調用到View的measure方法來開始View的測量。其實在measure方法中并沒有進行真正的測量操作,其內部只是根據傳入的兩個參數以及View內部的一些屬性進行一些邏輯判斷,來決定是否需要調用onMeasure方法來進行真正的測量操作。關于measure方法的源碼這里就不詳細講解,但需要知道的是它是一個被final修飾的方法,這也就意味著子類不能重寫它。這里貼出源碼中關于measure方法的解釋:

這個方法用于獲取一個View的大小,父布局在寬度和高度的參數中提供了約束信息;View的實際測量工作是在onMeasure方法中進行的,只是在這個方法內被調用。

既然實際的測量工作在onMeasure方法中,我們就來看一下其方法的源碼:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),

自定義view的三個方法。getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));

}

方法內只是調用了setMeasuredDimension方法,這個方法的作用就是存儲測量得到的寬度和高度,源碼的注釋中還提到此方法一定要在onMeasure中被調用,否則在測量時會觸發異常。現在我們再回過頭來看一下setMeasuredDimension方法中的兩個參數的由來,首先我們來看一下getSuggestedMinimumWidth和getSuggestedMinimumHeight方法:

protected int getSuggestedMinimumWidth() {

return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());

}

自定義View?protected int getSuggestedMinimumHeight() {

return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());

}

方法的內部就是判斷當前View是否設置了Drawable背景,如果沒有設置,就返回當前View的最小寬度(高度)。如果設置了背景,那就返回背景的最小寬度(高度)和View的最小寬度(高度)二者間的最大值。其中,mMinWidth和mMinHeight我們可以在xml中指定或者通過代碼動態設置。接下來再看看getDefaultSize方法:

public static int getDefaultSize(int size, int measureSpec) {

int result = size;

Android自定義復雜view。int specMode = MeasureSpec.getMode(measureSpec);

int specSize = MeasureSpec.getSize(measureSpec);

switch (specMode) {

case MeasureSpec.UNSPECIFIED:

result = size;

break;

自定義view的繪制流程、case MeasureSpec.AT_MOST:

case MeasureSpec.EXACTLY:

result = specSize;

break;

}

return result;

}

方法中的第二個參數是從measure方法中一路透傳過來的,關于它的值的由來前面已經闡述過。方法的內部調用MeasureSpec類的getMode和getSize方法獲取到測量模式和測量尺寸,然后根據測量模式返回對應的尺寸結果。

到這里,一個單獨View的measure過程可以說就講完了,但日常開發中一個View往往是放在一個ViewGroup容器中的,那么ViewGroup的measure過程又是怎么實現的呢?帶著這個疑問我們走進ViewGroup的源碼,我們會發現,在ViewGroup中根本沒有重寫onMeasure方法,也就是說沒有定義具體的測量邏輯,其實這也不難理解,因為ViewGroup本身是一個抽象類,而且它的子類的布局特性也都不盡相同,這也就導致它們測量的方式會有所不同,因此在ViewGroup中并沒有定義onMeasure方法的實現(在它的子類中可以看到onMeasure的實現)。不過在ViewGroup中,定義了一個measureChildren方法,其源碼如下:

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方法中就是遍歷自身所有的子View去調用measureChild方法,在measureChild方法的內部根據父布局對其寬度和高度的限制以及自身的LayoutParams來計算出自身的寬度和高度的MeasureSpec值,然后就是執行單個View的measure流程了。關于View的measure流程到這就全部講完了,在這里要記住幾個關于measure過程的注意點:

1.如果自定義View重寫了onMeasure方法,那么記得一定要在其內部調用setMeasuredDimension方法,否則會拋出IllegalStateException。

2.一個View的寬度和高度,不僅由自身的LayoutParams決定,還取決于父布局的約束。

3.View的measure方法是被final修飾的,不能被子類重寫。

4.布局--onLayout():

現在來看一下View繪制的第二個流程--布局,這個流程的作用就是父布局用來確定它的每一個子View的位置。在前面說到的performTraversals方法在執行完View的measure過程后,會繼續向下執行調用到View的layout方法,首先看一下layout方法的源碼:

public void layout(int l, int t, int r, int b) {

if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) { // step1

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

if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) { // step3

onLayout(changed, l, t, r, b);

........

}

........

}

這里只貼出了layout方法中部分關鍵的代碼,方法中的四個參數分別代表View相對于父容器的左、上、右、下的位置。接著走進方法內部,在step1處判斷是否需要在布局前進行測量操作,如果需要就重新調用onMeasure方法進行測量操作。然后在step2處判斷當前View的父布局是否有特殊的邊界(類似于陰影),如果有就執行setOpticalFrame方法,如果沒有就執行setFrame方法。而其實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;

........

mLeft = left;

mTop = top;

mRight = right;

mBottom = bottom;

........

}

return changed;

}

這里也只是貼出了一些關鍵的代碼,方法的主要作用就是兩個,第一將View相對于父布局的左、上、右、下位置信息保存(分別賦值給mLeft、mTop、mRight、mBottom),也就是說在這個方法中確定了View在父布局中的位置信息;第二就是根據對比之前的左、上、右、下位置信息來確定布局位置是否發生了改變。

現在,回到剛剛的layout方法中,在step3處,根據step2處返回的結果來決定是否需要調用onLayout方法。當我們在View的源碼中找到onLayout方法時,我們會發現它是一個空方法,這個方法的源碼注釋為:

當一個視圖需要為其每個子視圖分配大小和位置時,它會在layout方法中被調用;一個帶有子視圖的派生類需要重寫這個方法并讓其每個子視圖調用它的layout方法。

這也就能說明了在View的源碼中onLayout方法為什么是一個空方法了,因為它只是一個單獨的 View,不包含任何一個子View,所以也就無需去布局任何一個View。那誰才會包含子View呢?答案當然是ViewGroup了,我們走進ViewGroup的源碼尋找onLayout方法,結果如下:

@Override

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

居然是一個抽象方法,其實這個也不難理解,因為ViewGroup是一個抽象類,它沒有具體的布局特性,而它的子類像LinearLayout、RelativeLayout才有具體的布局特性,因此在ViewGroup的子類中我們才可以看到onLayout方法的具體實現。為了驗證這一點,這里我貼出FrameLayout中的onLayout方法的源碼,如下:

@Override

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

layoutChildren(left, top, right, bottom, false /* no force left gravity */);

}

void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {

final int count = getChildCount();

........

for (int i = 0; i < count; i++) {

final View child = getChildAt(i);

if (child.getVisibility() != GONE) {

........

child.layout(childLeft, childTop, childLeft + width, childTop + height);

}

}

}

在FrameLayout類的onLayout方法中,會調用其內部的layoutChildren方法,layoutChildren方法中略去的部分就是每個子View計算其相對于父布局的左、上、右、下位置信息的過程,因為本文意在講解View的整個繪制流程,所以這里就不過多的描述位置信息計算的細節了(感興趣的同學可以自己去看一下不同的布局控件的onLayout方法計算子View的位置信息的邏輯);在確定了位置信息后,最終再調用每個View的layout方法。到這里,View的layout流程就講完了,關于View的layout流程,也有幾個需要注意的點:

1.自定義View的時候盡量不要去重寫layout方法,否則可能會導致一些意外的繪制結果。

2.一個繼承了ViewGroup的子類,需要重寫onLayout方法并在內部調用每一個子View的layout方法。

5.繪制--onDraw():

到了View繪制的最后一個流程--繪制,在經歷了測量和布局兩個流程后,View的尺寸和位置都已經確定,就只差將View繪制到屏幕上了。首先,我們需要知道,我們最終將View繪制到了什么上面去。在開頭說到的performTraversals方法執行完layout流程后,會繼續向下執行調用到drawSoftware方法,在drawSoftware方法的內部會創建一個Canvas對象,最終這個Canvas對象會被傳入到View的draw方法中,而這個Canvas對象就是呈現視圖的畫布,也就是說最終View都會被繪制到這個畫布對象上去。接下來,我們先來看一下源碼中關于View的draw方法的解釋:

手動將視圖(及其所有子視圖)渲染到給定的畫布上。在調用此函數之前,視圖必須已經完成了完整的布局過程。當實現一個View的時候,去重寫onDraw方法而不是重寫這個draw方法;如果要重寫此方法,要調用超類的版本。

通過源碼中的注釋我們可以看出在我們使用自定義View的時候,系統是不建議我們重寫draw方法的,而是讓我們去重寫onDraw方法。由此我們可以先推測出一個結論,在draw方法中系統已經默認為我們提供了一套完整有序的繪制View到畫布上的流程,而onDraw方法中會根據不同的View的特性來采取不同的方式繪制View的具體內容。帶著這個推測我們來看一下draw方法的源碼,如下:

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

........

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

onDraw(canvas);

// Step 4, draw the children

dispatchDraw(canvas);

........

// Step 6, draw decorations (foreground, scrollbars)

onDrawForeground(canvas);

// Step 7, draw the default focus highlight

drawDefaultFocusHighlight(canvas);

........

return;

}

........

}

還是只貼出了關鍵部分的代碼,在方法中我們可以看到一段很長的注釋,這就是View在通常情況下的draw流程的一套完整的步驟(步驟2和步驟5通常情況下會跳過,這里就不做說明):

step1:調用drawBackground方法繪制View的背景(內部調用Drawable的draw方法)

step3:調用onDraw方法繪制View的具體內容

step4:調用dispatchDraw方法繪制子View

step6:調用onDrawForeground方法繪制裝飾(例如,滾動條)

step7:調用drawDefaultFocusHighlight方法繪制默認的焦點突出顯示

這里我們重點看一下步驟3和步驟4,在步驟3中調用的是View的onDraw方法,而View的onDraw方法是一個空方法,這就說明具體該怎么繪制View的內容要取決于View的子類的特性,因此要根據子類的特點來重寫onDraw方法實現繪制View內容的具體邏輯(TextView和ImageView的源碼中可以看到其根據自身的特點重寫了onDraw方法的邏輯)。現在,也可以證明剛剛我的推測是正確的了。

再來看看步驟4中的dispatchDraw方法,這個方法在View中也是一個空方法,因為一個單個的View是不存在子View的;而ViewGroup中重寫了這個方法,在方法的內部遍歷所有的子View執行drawChild方法,在drawChild方法的內部又調用了View的draw方法。到此,View的draw流程也講完了,關于View的draw流程,這里依然有幾個需要注意的點:

1.在View的源碼中系統已默認為我們提供了一套完整有序的draw流程,我們在自定義View的時候最好不要自己去重寫draw方法中的邏輯。

2.View的子類需要重寫onDraw方法,并根據自身的特點來編寫繪制View內容的邏輯。

結語

到這里,View的整個繪制流程就已經分析完了,其實View的源碼還是非常多的,文中提到的也只是View源碼中的冰山一角。如果想要對View有更深刻的理解,還是需要更深層次的閱讀它的源碼的。最后,如果文章對您有幫助,還望點贊支持下,有寫的不好的地方也望指出。

版权声明:本站所有资料均为网友推荐收集整理而来,仅供学习和研究交流使用。

原文链接:https://hbdhgg.com/5/113696.html

发表评论:

本站为非赢利网站,部分文章来源或改编自互联网及其他公众平台,主要目的在于分享信息,版权归原作者所有,内容仅供读者参考,如有侵权请联系我们删除!

Copyright © 2022 匯編語言學習筆記 Inc. 保留所有权利。

底部版权信息