带着问题去看源码——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也有关,一旦设置也是固定的了)。

欢迎到Github上查看全部文章Android-Knowledge

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,511评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,495评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,595评论 0 225
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,558评论 0 190
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,715评论 3 270
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,672评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,112评论 2 291
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,837评论 0 181
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,417评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,928评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,316评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,773评论 2 234
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,253评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,827评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,440评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,523评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,583评论 2 249