帶著問題去看原始碼——TextView篇
序言:為什麼會分析這個問題呢,因為上次釘釘電話面試中被面試官問到了,很尷尬的沒回答出來,View的繪製流程看過一點原始碼,但是感覺還不夠,像這種View的問題能夠延伸出很多問題,下面正文開始:
Q1:在一個RelativeLayout中有一個TextView和一個Button,當點選Button的時候給TextView設定文字,這時RelativeLayout會重新測量嗎?如果會,為什麼?
首先我們先大致的想一下這個問題問的是關於哪一塊的知識,如果毫不猶豫上去就是一通回答,這樣顯得太不明智了,我也知道會重新測量,為什麼?下面我們從原始碼的角度去看。既然是設定文字,那麼我們就從TextView的setText中去看看吧:
TextView:
private void setText(CharSequence text, BufferType type, boolean notifyBefore, int oldlen) {
...
if (mLayout != null) {
checkForRelayout();
}
...
}
在setText中,我找到了這個checkForRelayout
方法,由於我們初始化過了,所以setText肯定會執行該方法:
TextView:
/**
* Check whether entirely new text requires a new view layout
* or merely a new text layout.
* 檢查新文字是否需要一個新的檢視佈局
*/
private void checkForRelayout() {
// If we have a fixed width, we can just swap in a new text layout
// if the text height stays the same or if the view height is fixed.
//如果textview的寬度和高度固定不變的話
if ((mLayoutParams.width != LayoutParams.WRAP_CONTENT
|| (mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth))
&& (mHint == null || mHintLayout != null)
&& (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)) {
// Static width, so try making a new text layout.
int oldht = mLayout.getHeight();
int want = mLayout.getWidth();
int hintWant = mHintLayout == null ? 0 : mHintLayout.getWidth();
/*
* No need to bring the text into view, since the size is not
* changing (unless we do the requestLayout(), in which case it
* will happen at measure).
* 因為大小不會變,所以不需要將文字放入檢視中
*/
makeNewLayout(want, hintWant, UNKNOWN_BORING, UNKNOWN_BORING,
mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(),
false);
if (mEllipsize != TextUtils.TruncateAt.MARQUEE) {
// In a fixed-height view, so use our new text layout.
if (mLayoutParams.height != LayoutParams.WRAP_CONTENT
&& mLayoutParams.height != LayoutParams.MATCH_PARENT) {
autoSizeText();
invalidate();
return;
}
// Dynamic height, but height has stayed the same,
// so use our new text layout.
//動態高度,但是高度不變
if (mLayout.getHeight() == oldht
&& (mHintLayout == null || mHintLayout.getHeight() == oldht)) {
autoSizeText();
invalidate();
return;
}
}
// We lose: the height has changed and we have a dynamic height.
// Request a new view layout using our new text layout.
requestLayout();
invalidate();
} else {
// Dynamic width, so we have no choice but to request a new
// view layout with a new text layout.
//動態寬度,我們只能請求一個新的佈局
nullLayouts();
requestLayout();
invalidate();
}
}
說實話,看原始碼真的是一件很累的過程,我們可能很難找到下手的點,在這裡給大家分享一個方法,找你覺得是重點的程式碼或者方法去看(和你本次看原始碼想要研究的方向相同),一旦你看著看著發現看不太懂了,你就倒回來再看其他地方。從上面這段程式碼我們不難看出,在這裡呼叫了requestLayout()
和invalidate()
這兩個方法,看到這裡相信大家應該就明白了吧,是他是他就是他,requestLayout()
,好,我們也順便來看一下這個方法:
TextView:
public void requestLayout() {
//判斷是否正在佈局
if (mMeasureCache != null) mMeasureCache.clear();
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
// Only trigger request-during-layout logic if this is the view requesting it,
// not the views in its parent hierarchy
ViewRootImpl viewRoot = getViewRootImpl();
if (viewRoot != null && viewRoot.isInLayout()) {
if (!viewRoot.requestLayoutDuringLayout(this)) {
return;
}
}
mAttachInfo.mViewRequestingLayout = this;
}
mPrivateFlags |= PFLAG_FORCE_LAYOUT;
mPrivateFlags |= PFLAG_INVALIDATED;
if (mParent != null && !mParent.isLayoutRequested()) {
//向父容器請求佈局
mParent.requestLayout();
}
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
mAttachInfo.mViewRequestingLayout = null;
}
}
在requestLayout()
方法中呼叫了mParent.requestLayout()
,也就是說呼叫了父View的requestLayout()
方法,然後一級一級往上傳,最終會呼叫ViewRootImpl中的requestLayout()
方法:
RootViewImpl:
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
首先會先去判斷一下是否是在當前執行緒,然後會呼叫scheduleTraversals()
方法:
RootViewImpl:
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
看到這裡估計有很多人會懵逼了,這裡好像也沒有什麼嘛,別急老鐵,這裡有一個名叫mTraversalRunnable
的引數,那我們就點進去看看他的實現:
RootViewImpl:
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
接下來會呼叫doTraversal()方法:
RootViewImpl:
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
然後終於到了我們所期待的地方了,這裡又呼叫了performTraversals()
方法,相信看過View的繪製流程原始碼的童鞋應該就知道了,這裡才真正開始View的測量,擺放,繪製等操作。在performTraversals()
這個方法中分別呼叫了onMeasure,onLayout,onDraw等方法,有興趣的童鞋可以自行去看。至此我們應該就能知道上述問題該如何回答了。
Q2:為什麼TextView的寬高設定成wrap_content,在Activity中獲取的時候寬度為0,高度不為0?
這個問題呢,是我在找上面那個問題的答案的過程中發現的,既然是寬高的問題,那麼我們當然得要去看onMeasure方法了:
if (widthMode == MeasureSpec.EXACTLY) {
// Parent has told us how big to be. So be it.
width = widthSize;
} else {
//寬度設定成wrap_content會走這裡
if (mLayout != null && mEllipsize == null) {
des = desired(mLayout);
}
if (des < 0) {
boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, mBoring);
if (boring != null) {
mBoring = boring;
}
} else {
fromexisting = true;
}
if (boring == null || boring == UNKNOWN_BORING) {
if (des < 0) {
des = (int) Math.ceil(Layout.getDesiredWidth(mTransformed, 0, mTransformed.length(), mTextPaint, mTextDir));
}
} else {
width = boring.width;
}
...
這下面是計算設定drawable和hint的寬度,所以我們可以忽略
}
關於寬度的我們只需要看這一段就好了,TextView設定wrap_content,會走下面的else,然後第一次進來,這個onMeasue裡面的mLayout還沒有初始化,所以mLayout = null
,然後由於des = -1
,所以boring會被初始化,boring != null
,所以width = boring.width
,而boring.width這個東西初始值為0,所以width = 0
;同樣的高度也是這樣分析就可以了,要注意的是,高度和textSize和行數有關,所以設定不同的行數和textSize(預設是有TextSize的)得到的hight都不一樣的。
總結:其實大家可以這樣理解,寬高都設定成wrap_content,沒設定文字的情況下,寬度肯定為0,但是單行的高度是固定的(和TextSize也有關,一旦設定也是固定的了)。