1. 程式人生 > >帶著問題去看原始碼——TextView篇

帶著問題去看原始碼——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?

image

這個問題呢,是我在找上面那個問題的答案的過程中發現的,既然是寬高的問題,那麼我們當然得要去看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也有關,一旦設定也是固定的了)。