關于TextView中maxLines替換singleLine遇到的坑

關于TextView的使用,作為一個Android開發者來說,想必大家已經用到想吐了。但是這個TextView可以說是最簡單也是最復雜的View之一,首先說它簡單,因為在界面上它卻是非常簡單,就一個破文本,沒啥值得炫耀的。然后說它復雜,是因為。。。不說了,自己打開TextView的源碼瞅瞅吧!(1個類一萬多行代碼,你跟我說不復雜試試~_~)

坑是這樣的

廢話不多說了,最近碰到一個很坑爹的問題,因為開發過程中lint靜態掃描代碼老提示singleLine已經廢棄,讓我用maxLines="1"替換。我就想著,這么簡單,換就換唄,萬萬沒想到,越是這么簡單越讓人掉坑。咱先上圖:


使用singleLine效果

效果很簡單,一行字符串單行顯示,末尾用省略號顯示,沒毛??!咱看看布局文件是怎么寫的:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    
    android:layout_width="match_parent"    
    android:layout_height="match_parent"    
    android:orientation="horizontal">    
    <TextView        
        android:layout_width="200dp"        
        android:layout_height="wrap_content"        
        android:background="#adddcc"        
        android:singleLine="true"       
        android:text="父視圖在對子視圖進行measure操作的過程中,使用變量mTotalLength保存已經measure過的child所占用的高度,該變量剛開始時是0。在for循環中調用measureChildBeforeLayout" />
</LinearLayout>

布局也是簡單的不要不要的,然而AndroidStudio確給我這樣的提示。

AS的自動提示

看著挺簡單的嘛,不就是用maxLines="1"替換singleLine="true",好,咱們就用maxLines來替換來看下效果,修改后的xml如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"             
    android:layout_width="match_parent"        
    android:layout_height="match_parent"    
    android:orientation="horizontal">    
    <TextView        
        android:layout_width="match_parent"            
        android:layout_height="wrap_content"        
        android:background="#adddcc"        
        android:maxLines="1"        
        android:text="父視圖在對子視圖進行measure操作的過程中,使用變量mTotalLength保存已經measure過的child所占用的高度,該變量剛開始時是0。在for循環中調用measureChildBeforeLayout" />
</LinearLayout>

嗯,很好,運行下試試。

使用maxLines="1"的效果

尼瑪,這效果有點坑爹啊,為啥后面會留有一坨空白的,而且省略號也沒有了?

真的是嚇得我趕緊看看API來壓壓驚:



singleLine主要是約束文本顯示在水平的一行,而maxLines則負責TextView高度最多只有一行高。看來maxLine并不關心省略號怎么顯示啊,這里可能很多人都知道要結合ellipsize屬性使用才能看到省略號效果。好,我們來試試:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"             
    android:layout_width="match_parent"        
    android:layout_height="match_parent"    
    android:orientation="horizontal">    
    <TextView        
        android:layout_width="match_parent"            
        android:layout_height="wrap_content"        
        android:background="#adddcc"        
        android:maxLines="1"      
        android:ellipsize="end"  
        android:text="父視圖在對子視圖進行measure操作的過程中,使用變量mTotalLength保存已經measure過的child所占用的高度,該變量剛開始時是0。在for循環中調用measureChildBeforeLayout" />
</LinearLayout>
maxLines="1"結合ellipsize="end"使用效果

效果還是很不錯的嘛!
是的,但是回到之前沒有設置ellipsize屬性的時候,為什么后面會留出一坨空白?我們現在就這個問題來分析分析。咱們首先該下布局文件如下,

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"             
    android:layout_width="match_parent"        
    android:layout_height="match_parent"    
    android:orientation="horizontal">    
    <TextView        
        android:layout_width="match_parent"            
        android:layout_height="wrap_content"        
        android:background="#adddcc"        
        android:maxLines="3"      
        android:ellipsize="end"  
        android:text="父視圖在對子視圖進行measure操作的過程中,使用變量mTotalLength保存已經measure過的child所占用的高度,該變量剛開始時是0。在for循環中調用measureChildBeforeLayout" />
 <TextView        
        android:layout_width="match_parent"            
        android:layout_height="wrap_content"        
        android:layout_marginTop="20dp"
        android:background="#adddcc"        
        android:maxLines="3"      
        android:ellipsize="end"  
        android:text="adfadfadgfadsfgfsdhfhdfghdfghdfga dfagsdfgsfghfghdfghsdfghsfgfghdfghsdfghs/fgfghdfghsdfghsfgghsfgfghdfghsdfghsfgghsfgfghdfghsdfghsfgghsfgfghdfghsdfghsfg" />
</LinearLayout>

再來看看效果吧


文本中加入空格 / ? !等特殊字符效果

看到上面這效果,我的眼淚都快掉了,為啥無緣無故給我亂折行?第一個TextView中有中文字符和英文字符混搭,結果文本繪制就有問題了。第二個是全英文字符,然后和一些特殊字符如空格、斜杠、問號等混搭,也出現亂折行的問題。而且還跟特殊字符的位置有關,我們這里把第二個TextView中文本的斜杠放到空格后兩個試試效果:


調整特殊字符的位置效果

尼瑪,現在第二個TextView又不折行了,真的是想死的心都有了。。。
你以為坑就到這了,太天真了,來來來,我們在上面的基礎上再修改下文本,在后面加個斜杠試試:


在后面加了個斜杠的效果

哎哎哎,哥們,陽臺在哪里,別攔著我。。。

翻翻源碼吧

好吧好吧,咱們來看看源碼這是咋回事,打開那個TextView,看著一萬多行代碼,從哪里開始看呢。問題在哪里咱們就從哪里開始,由于文本最終是通過onDraw方法繪制出來的,咱就先看下這個方法,看看text文本是怎么繪制出來的。然而并沒有發現canvas.drawText等類似方法,但是有幾句代碼還是比較可疑

@Overrideprotected void onDraw(Canvas canvas) {
    ...
    if (mLayout == null) {    
        assumeLayout();    //1
    }
    Layout layout = mLayout;
    if (mEditor != null) {   
        mEditor.onDraw(canvas, layout, highlight, mHighlightPaint, cursorOffsetVertical);
    } else {  
        layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);    //2
    }
}

因為mEditor為空,所以咱們只要看這個layout,這個layout是個啥東西呢,看1的方法assumeLayout()。

/** * Make a new Layout based on the already-measured size of the view, 
    * on the assumption that it was measured correctly at some point. */
private void assumeLayout() {    
    int width = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();   
    if (width < 1) {        
        width = 0;    
    }    
    int physicalWidth = width;    
    if (mHorizontallyScrolling) {        
        width = VERY_WIDE;    
    }    
    makeNewLayout(width, physicalWidth, UNKNOWN_BORING, UNKNOWN_BORING, physicalWidth, false);
}

繼續看makeNewLayout這個方法,

/** * The width passed in is now the desired layout width, * not the full view width with padding. * {@hide} */
protected void makeNewLayout(int wantWidth, int hintWidth,                             BoringLayout.Metrics boring,                             BoringLayout.Metrics hintBoring,                             int ellipsisWidth, boolean bringIntoView) {
    ...
    mLayout = makeSingleLayout(wantWidth, boring, ellipsisWidth, alignment, shouldEllipsize,   effectiveEllipsize, effectiveEllipsize == mEllipsize);    
    ...    
}

我們主要看這個layout的初始化,所以進入makeSingleLayout方法,

private Layout makeSingleLayout(int wantWidth, BoringLayout.Metrics boring, int ellipsisWidth,        Layout.Alignment alignment, boolean shouldEllipsize, TruncateAt effectiveEllipsize,        boolean useSaved) {    
    Layout result = null;    
    if (mText instanceof Spannable) {            //1
        result = new DynamicLayout(mText, mTransformed, mTextPaint, wantWidth, alignment, mTextDir, mSpacingMult, mSpacingAdd, mIncludePad,                mBreakStrategy, mHyphenationFrequency,  getKeyListener() == null ? effectiveEllipsize : null, ellipsisWidth);   
    } else {        
    if (boring == UNKNOWN_BORING) {            //2、
        boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, mBoring);           
        if (boring != null) {                
            mBoring = boring;            
        }        
    }        
    if (boring != null) {            
        if (boring.width <= wantWidth && (effectiveEllipsize == null || boring.width <= ellipsisWidth)) {                
            if (useSaved && mSavedLayout != null) {                    
                result = mSavedLayout.replaceOrMake(mTransformed, mTextPaiwantWidth, alignment, mSpacingMult, mSpacingAdd,  boring, mIncludePad);                
            } else {                    
                result = BoringLayout.make(mTransformed, mTextPaint, wantWidth, alignment, mSpacingMult, mSpacingAdd,  boring, mIncludePad);                
            }                
            if (useSaved) {                    
                mSavedLayout = (BoringLayout) result;                
            }            
        } else if (shouldEllipsize && boring.width <= wantWidth) {                
            if (useSaved && mSavedLayout != null) {                    
                result = mSavedLayout.replaceOrMake(mTransformed, mTextPaint,wantWidth, alignment, mSpacingMult, mSpacingAdd, boring, mIncludePad, effectiveEllipsize, ellipsisWidth);                
            } else {                   
                 result = BoringLayout.make(mTransformed, mTextPaint, wantWidth, alignment, mSpacingMult, mSpacingAdd, boring, mIncludePad, effectiveEllipsize, ellipsisWidth);                
            }            
        }        
    }       
    if (result == null) {        
        StaticLayout.Builder builder = StaticLayout.Builder.obtain(mTransformed, 0, mTransformed.length(), mTextPaint, wantWidth)
            .setAlignment(alignment).setTextDirection(mTextDir)
            .setLineSpacing(mSpacingAdd, mSpacingMult)
            .setIncludePad(mIncludePad).setBreakStrategy(mBreakStrategy)
            .setHyphenationFrequency(mHyphenationFrequency);       
        if (shouldEllipsize) {            
            builder.setEllipsize(effectiveEllipsize)
            .setEllipsizedWidth(ellipsisWidth) 
            .setMaxLines(mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);        
        }        
        // TODO: explore always setting maxLines      
        result = builder.build();        //3、
    }    
    return result;
}

看到上面的3個注釋,都是xxxLayout,結合onDraw方法里的layout.draw(...),我們便能推測出TextView繪制文本的過程應都是交給這個Layout去完成的。于是趕緊去安利下這個Layout,這個layout主要有3個子類
1、BoringLayout 主要負責顯示單行文本,并提供了isBoring方法來判斷是否滿足單行文本的條件。

2、DynamicLayout 當文本為Spannable的時候,TextView就會使用它來負責文本的顯示,在內部設置了SpanWatcher,當檢測到span改變的時候,會進行reflow,重新計算布局。

3、StaticLayout 當文本為非單行文本,且非Spannable的時候,就會使用StaticLayout,內部并不會監聽span的變化,因此效率上會比DynamicLayout高,只需一次布局的創建即可,但其實內部也能顯示SpannableString,只是不能在span變化之后重新進行布局而已。

到了這里,大概能知道上面那個坑出在哪塊了,由于之前給定的text為非單行文本,而且非Spannable,所以我們具體看看這個StaticLayout。根據UI界面顯示的效果和問題所在,可以斷定出問題的應該是在折行處理這一塊。然后debug,發現generate方法的這句代碼:

void generate(Builder b, boolean includepad, boolean trackpad) {
    ...
    int breakCount = nComputeLineBreaks(b.mNativePtr, lineBreaks, lineBreaks.breaks, lineBreaks.widths, lineBreaks.flags, lineBreaks.breaks.length);
    ...
}

這個方法的作用是處理文本折行的問題,無奈是個JNI方法,然后我看了一短時間并沒有搞懂google的折行機制,不過發現這個折行跟很多特殊字符有關(! $ - + 空格 } | / ? 等等)。。。
不過textview的折行包含以下規律:
1、半角字符與全角字符混亂所致:這種情況一般就是漢字與數字、英文字母混用。
2、TextView在顯示中文的時候標點符號不能顯示在一行的行首和行尾,如果一個標點符號剛好在一行的行尾,該標點符號就會連同前一個字符跳到下一行顯示。
3、一個英文單詞不能被顯示在兩行中( TextView在顯示英文時,標點符號是可以放在行尾的,但英文單詞也不能分開 )。
至于解決方案,網上有一種http://niufc.iteye.com/blog/1729792, 但是效果不佳,git上也有一個行文本左右對齊的自定義TextViewhttps://github.com/ufo22940268/android-justifiedtextview, 但是效果也不是非常好。

這里還有個問題值得思考,為什么maxLine="1"加上ellipsize="end"屬性后,就能正常出現省略號還不留白?

這是因為,StaticLayout中針對ellipsize屬性,對文本內容單獨進行了處理(之前的折行處理效果在這里就不管用了),然后Layout.onDraw的時候只會繪制處理完后的text。具體方法在out中

private int out(CharSequence text, int start, int end, int above, int below, int top, int bottom, int v,  float spacingmult, float spacingadd,   
               LineHeightSpan[] chooseHt, int[] chooseHtv, Paint.FontMetricsInt fm, int flags, boolean needMultiply, byte[] chdirs, int dir, 
               boolean easy, int bufEnd, boolean includePad,boolean trackPad, char[] chs, 
               float[] widths, int widthStart, TextUtils.TruncateAt ellipsize,                  float ellipsisWidth, float textWidth, TextPaint paint, boolean moreChars) {
    ...
    if (ellipsize != null) {   
        // If there is only one line, then do any type of ellipsis except when it is MARQUEE    
        // if there are multiple lines, just allow END ellipsis on the last line   
        boolean forceEllipsis = moreChars && (mLineCount + 1 == mMaximumVisibleLineCount);    
        boolean doEllipsis = (((mMaximumVisibleLineCount == 1 && moreChars) || (firstLine && !moreChars)) &&  ellipsize != TextUtils.TruncateAt.MARQUEE) ||                (!firstLine && (currentLineIsTheLastVisibleOne || !moreChars) &&                        ellipsize == TextUtils.TruncateAt.END);    
        if (doEllipsis) {        
            calculateEllipsis(start, end, widths, widthStart, ellipsisWidth, ellipsize, j,                textWidth, paint, forceEllipsis);    
        }
    }
    mLineCount++;
    return v;
}

大家感興趣繼續看calculateEllipsis這個方法吧。。。
說了那么多,其實最主要的坑還是google的折行算法有問題,

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,362評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,013評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 177,346評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,421評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,146評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,534評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,585評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,767評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,318評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,074評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,258評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,828評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,486評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,916評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,156評論 1 290
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,993評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,234評論 2 375

推薦閱讀更多精彩內容