TextView 的新特性,Autosizing 到底是如何實現的? | 原始碼分析

承香墨影發表於2018-01-15

194

一、前言

Hi,大家好,我是承香墨影!

前兩天聊了一下 Autosizing 的使用,反映還不錯。畢竟是這種能解決實際問題的新 Api,確實在需要的時候,用起來會很順手。

簡單回顧一下,Autosizing 是在 Support v26 中新支援的功能,可以根據文字的內容和 TextView 的大小,自動適應齊內部文字的字型大小,來達到完全顯示的效果。而這個功能,最低能相容到 Api Level 14,可以說是一個誠意滿滿的新 Api。

TextView 的新特性,Autosizing 到底是如何實現的? | 原始碼分析

還不瞭解 Autosizing 的朋友,可以看看之前的文章《文字太多?控制元件太小?試試 TextView 的新特性 Autosizeing 吧!》,裡面有使用它的詳細介紹。

我想,在沒有 Autosizing 的時候,應該已經有人以這樣的思路在實現功能了。那麼,今天就來從原始碼的角度分析一下,Autosizing 的原理如何,看看它是如何工作的。

二、帶著問題看原始碼

分析原始碼也是講究方式方法的,我主推的一個思路,就是帶著問題看原始碼。

很多大型專案,其實本身都是很複雜的,並且涵蓋的功能點也非常的多,如果想要一次就把它完整的閱讀屢清楚,還是很吃力的。

所以我建議在閱讀開源專案之前,你先閱讀文件,嘗試使用一下它,看看它能做什麼,再自己思考一下,如果你是作者,你會如何去實現這些功能的,最後帶著這些問題去閱讀原始碼,以問題為出發點,看看那些大牛寫的優秀的開源庫,到底有什麼值得我們借鑑的地方。

總歸一句話:閱讀原始碼是為了更好的編寫原始碼!

當我看到到 Autosizing 這個新特性的時候,我有一些好奇的地方在於:

  1. Android 8.0 的 TextView 和 Support 包中,Autosizing 的實現,有什麼區別?
  2. Autosizing 是會在什麼時機,去觸發根據文字的內容,計算出一個適合的字型大小。
  3. Autosizing 是如何計算合適的字型大小的。
  4. 脫離 Autosizing,原始碼中的功能,有什麼能借鑑的使用場景。

大概就是這些問題吧,接下來我們看看 Autosizing 是如何實現的。

三、Autosizing 原始碼

3.1 實現的區別

對於 Android 8.0 中和 Support v26 中,具體對於 Autosizing 的實現,有什麼區別這一點,大致閱讀一下兩邊的原始碼,你會發現大致上沒區別。

它們之間,和 Autosizing 相關的原始碼所在的原始碼檔案也不一樣:

  • Android 8.0 主要在 TextView 中。
  • Support v26 主要在 AppCompatTextViewAutoSizeHelper。

隨手比對一下它們的 setAutoSizeTextTypeWithDefaults() 方法,這個方法用來標記是否對 TextView 開啟 Autosizing。

setAutoSizeDefaultDiff

左邊是 Android 8.0 的 TextView ,右邊是 AppCompatTextViewAutoSizeHelper。

可以看到,整個程式碼的結構都是一致的,只是部分引用的類不一樣而已,但是表達的意思是一致的。

之所以說它們之前大致是一樣的,是因為有一些 Api 是 private 的或者被標記為 @hide 了,這樣,在外部是無法訪問到的。對此 AppCompatTextViewAutoSizeHelper 的做法是用反射的形式去呼叫它。

例如,實際去修改 TextView 尺寸的方法 autoSizeText() ,看下面它們的區別。

autoSizeDiff

左邊是 Android 8.0 的 TextView ,右邊是 AppCompatTextViewAutoSizeHelper。

兩邊都需要獲取 mHorizontallyScrolling 的值,TextView 內部當然可以直接呼叫了,而 AppCompatTextViewAutoSizeHelper 的做法是,使用 invokeAndReturnWithDefault() 方法,通過反射區獲取這個值。

invokeMethod

所以,我們可以得出結論,兩邊的實現思路,大體上是沒有區別的,只是有一些小細節,會不一樣,但是我們不需要太在意這些。

既然,兩邊的區別不大,之後我們就以 Support v26 中,關於 Autosizing 的原始碼實現來進行分析。

3.2 觸發 Autosizing 的時機

首先這個時機讓我自己來設計,也非常好理解。

本質上 Autosizing 就是為了讓 TextView 中的文字,能完全顯示,在這個過程中會去調整 文字 的字型大小。

那這樣,觸發它的時機,其實就很容易猜到了:

  1. 在 文字內容 變動的時候。
  2. 在 TextView 大小變動的時候。

Support v26 中,之所以能保證相容,本質上它會自動將 TextView 這樣的控制元件,替換成 AppCompatTextView 來達到相容的效果,這個過程中,開發者只需要使用 AppCompatActivity 就可以了,其它的不需要開發者來參與。這樣,其實我們只需要關注 AppCompatTextView 中的實現邏輯就好了。

前面提到,操作 Autosizing 的具體原始碼,在 AppCompatTextViewAutoSizeHelper 中。 而 AppCompatTextView 並不直接操作它。

首先 AppCompatTextVIew 會持有 AppCompatTextHelper 這個幫助類,而這個幫助類,又去持有 AppCompatTextViewAutoSizeHelper,最終所有的邏輯都傳遞到 AppCompatTextViewAutoSizeHelper 中去處理。

所以操作的流程大概是這樣的:

stream

而 Autosizing 真實去測量並修改字型大小的邏輯,都在 autoSizeText() 方法中,我們只需要關心它在何時被呼叫,就能知道具體觸發 Autosizing 的時機了。

第一個觸發點,它會在 AppCompatTextView 的 onTextChanged() 方法中,直接呼叫 autoSizeText() 方法。

AppCompatAutoTextSize

第二個觸發點,會監聽 AppCompatTextView 的 onLayout() 方法,在其中呼叫 AppCompatTextHelper 的 onLayout() 方法。

helper_onLayout

好了,兩個時機都找到了,也驗證了我們之前的猜想。

3.3 Autosizing 如何計算大小

前面提到 Autosizing 實際上去修改 TextView 字型的方法,在 AppCompatTextViewAutoSizeHelper 的 autoSizeText() 方法中,這裡我們先來看看這個方法的實現。

autoSizeText

這一段邏輯,就是 Autosizing 中,很重要的一個邏輯。先來看看它大體上的流程。

  1. 使用它會使用 isAutoSizeEnabled() 方法,判斷當前是否開啟 Autosizing 。
  2. 判斷 mNeedsAutoSizeText 是否為 true,此處判斷是主要是看是否存在可變動的尺寸。
  3. 計算 TextView 本身的顯示區域大小,存放在 TEMP_RECTF 中。
  4. 使用 findLargestTextSizeWhichFits() 獲取到一個合適當前文字長度的最大尺寸值。
  5. 如果和當前 TextView 的 textSize 不一致,則使用 setTextSizeInternal() 將其設定回去。

大體步驟就是這樣,接下來我們從細節出發看看它的具體實現。

首先是 isAutoSizeEnable() 方法,它去判斷當前是否開啟了 Autosizing,其實就是判斷 mAutoSizeTextType 屬性是否為 none。

isAutoSizeEnable

mNeedsAutoSizeText 這個判斷,本質上其實是為了判斷 mAutoSizeTextSizesInPx 這個存放尺寸的陣列裡,是否有值,這個尺寸陣列,在後面的 findLargestTextSizeWhichFits() 方法中會用到。

px-array

mAutoSizeTextSizesInPx 其實就是一個存放當前 TextView 預估能使用的尺寸陣列,是被提前計算出來的,它會在對 Autosizing 受影響的相關的屬性做出修改的時候,重新計算。例如:粒度(Granularity)、預設尺寸(PresetSizes)等變動,都會觸發重新計算 mAutoSizeTextSizesInPx 的值。

TEMP_RECTF 就沒有什麼好說的了,無非就是從 TextView 的寬高和 Padding 等屬性,計算出一個能用於顯示 文字 區域大小。接下來就會去呼叫 findLargestTextSizeWhichFits() 方法,找到一個當前 文字 內容,最合適的字型大小。

findLargestMethod

這裡邏輯也很清晰,就是使用一個迴圈,通過 suggestedSizeFitsInSpace() 方法判斷取出來的尺寸是否合適。這裡為了提高效率,使用了二分演算法,去避免全部遍歷 mAutoSizeTextSizesInPx 陣列,從而提高效率。

接下來就是 suggestedSizeFitsInSpace() 方法,它會根據 TextView 的內容區域和 文字,判斷當前給定的尺寸,是否能放的下這些內容。

suggestedSizeMethod

這裡首先使用了一個 TextPaint 物件 mTempTextPaint 來存放 TextView 的一些引數,然後根據 mTempTextPaint 去建立一個使用 StaticLayout 物件,來嘗試對文字進行佈局。

StaticLayout 是一個為不可編輯的文字佈局的類,這意味著一旦佈局完成,文字內容就不可以改變。

最終,就能確定,傳遞進行的字型大小,是否能完全顯示在這個區域內。

經過這一通計算,findLargestTextSizeWhichFits() 方法,最終將計算出來的一個合適的字型尺寸,返回回去,再通過 setTextSizeInternal() 設定到 TextView,來達到修改字型大小的目的。

四、原始碼中能借鑑的功能

現在來看,Autosizing 計算某段文字,在一個 固定的 TextView 中,將展示的單行寬度和行數這個功能,這些算是 Autosizing 中,比較有借鑑意義的功能了。

其它的我暫時沒有想到,你覺得還有什麼可以借鑑的點呢?在留言中告訴我。

今天在承香墨影公眾號的後臺,回覆『成長』,我會送你一些特別的內容。

我另外還維護了一個技術交流的微信群,有興趣可以在公眾號後臺回覆:"加群"

推薦閱讀:

TextView 的新特性,Autosizing 到底是如何實現的? | 原始碼分析

相關文章