Android:TextView的AutoSizeText源碼實現

在實際開發中,我們經常會遇到,TextView區域固定,但字數不確定,又希望把文字信息都展示出來,

IOS中可以輕鬆實現,但在Android上好想並沒有這種實現。

8.0(API26)開始,TextView組件提供了autoSizeTextType、autoSizeMinTextSize、autoSizeMaxTextSize等相關屬性,來滿足這個需求,在8.0以前的版本,可以通過com.android.support:appcompat-v7來實現。

那麼要實現這個功能,對於TextView的佈局設置,也是有要求的。

1、不要設置行數(包括MaxLines),包括SingleLine

2、高度不要設置wrap_content,要設置固定值。

3、對於8.0以前的版本,xml佈局文件中要使用android.support.v7.widget.AppCompatTextView

android的設置,除了設置autoSizeTextType=uniform,還要設置autoSizeMinTextSize、autoSizeMaxTextSize,系統有默認值。這兩個參數,個人感覺有些多餘,比起IOS有點繁瑣。因為字數不固定,你也不知道到底字號小到什麼程度,才能完全展示,所以

autoSizeMinTextSize我們只能設置一個儘量小的值。如果最大最小值設置不合適,一樣達不到效果。

當在xml文件設置好上面的屬性後,在執行TextView的構造函數時,如果設置了autoSize相關屬性,那麼會執行setupAutoSizeText方法,進行一些屬性的設置。

<code>private boolean setupAutoSizeText() {
if (supportsAutoSizeText() && mAutoSizeTextType == AUTO_SIZE_TEXT_TYPE_UNIFORM) {
if (!mHasPresetAutoSizeValues || mAutoSizeTextSizesInPx.length == 0) {
int autoSizeValuesLength = 1;
float currentSize = Math.round(mAutoSizeMinTextSizeInPx);
while (Math.round(currentSize + mAutoSizeStepGranularityInPx)
<= Math.round(mAutoSizeMaxTextSizeInPx)) {
autoSizeValuesLength++;
currentSize += mAutoSizeStepGranularityInPx;
}

int[] autoSizeTextSizesInPx = new int[autoSizeValuesLength];
float sizeToAdd = mAutoSizeMinTextSizeInPx;
for (int i = 0; i < autoSizeValuesLength; i++) {
autoSizeTextSizesInPx[i] = Math.round(sizeToAdd);
sizeToAdd += mAutoSizeStepGranularityInPx;
}
mAutoSizeTextSizesInPx = cleanupAutoSizePresetSizes(autoSizeTextSizesInPx);
}

mNeedsAutoSizeText = true;
} else {
mNeedsAutoSizeText = false;
}

return mNeedsAutoSizeText;
}/<code>

大概的功能就是根據最大值,最小值這個區間,然後有一個步進值,從最小值開始,每次加上這個步進值,然後跟最大值比較,如果小於,就繼續加步進值,再次比較,一直循環。

mAutoSizeStepGranularityInPx就是步進值,默認是1。autoSizeValuesLength是一個計數,表示進行了多少次累加,後面會根據這個值創建一個數組autoSizeTextSizesInPx。然後再次從最小值開始,每次加上步進值,把結果添加到數組對應的位置上。

數組填充完後,還要通過cleanupAutoSizePresetSizes做一些處理,保證數組裡面值是唯一的,並且是排序好的,cleanupAutoSizePresetSizes中用到了二分法,返回的結果賦值給屬性mAutoSizeTextSizesInPx。同時會設置mNeedsAutoSizeText屬性,表示需要進行自動適應字體。

隨後,在TextViewlayout方法時,就會調用內部的autoSizeText方法。

<code>protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
......
// Call auto-size after the width and height have been calculated.
autoSizeText();
}/<code>

這個方法中,首先會檢查是否支持自動適應字體的功能。

<code>private boolean isAutoSizeEnabled() {
//supportsAutoSizeText默認返回true
return supportsAutoSizeText() && mAutoSizeTextType != AUTO_SIZE_TEXT_TYPE_NONE;
}/<code>

然後判斷mNeedsAutoSizeText,如果需要調整,就會通過findLargestTextSizeWhichFits來找到最適合的字體大小。

<code>    private void autoSizeText() {
if (!isAutoSizeEnabled()) {
return;
}
//判斷mNeedsAutoSizeText
if (mNeedsAutoSizeText) {
//判斷長度和寬度,非正數,退出
if (getMeasuredWidth() <= 0 || getMeasuredHeight() <= 0) {
return;
}
/*
*計算可用寬度,如果是可以橫向滾動的,那麼取一個很大的值VERY_WIDE
*否則就是用寬度減去左右的pandding值
*/
final int availableWidth = mHorizontallyScrolling
? VERY_WIDE
: getMeasuredWidth() - getTotalPaddingLeft() - getTotalPaddingRight();
/*
*計算可用高度,高度減去上下的pandding值
*/
final int availableHeight = getMeasuredHeight() - getExtendedPaddingBottom()
- getExtendedPaddingTop();

//不合法,退出

if (availableWidth <= 0 || availableHeight <= 0) {
return;
}
//設置區域的值
synchronized (TEMP_RECTF) {
TEMP_RECTF.setEmpty();
TEMP_RECTF.right = availableWidth;
TEMP_RECTF.bottom = availableHeight;
/*findLargestTextSizeWhichFits,查找最適合的字體大小
*
*/
final float optimalTextSize = findLargestTextSizeWhichFits(TEMP_RECTF);
//返回的size如果和你設置的size不一樣,那麼重新設置新的size,保證文字顯示全部
if (optimalTextSize != getTextSize()) {
setTextSizeInternal(TypedValue.COMPLEX_UNIT_PX, optimalTextSize,false);

makeNewLayout(availableWidth, 0, UNKNOWN_BORING, UNKNOWN_BORING,mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(),false);
}
}
}
// Always try to auto-size if enabled. Functions that do not want to trigger auto-sizing
// after the next layout pass should set this to false.
mNeedsAutoSizeText = true;
}/<code>

來看findLargestTextSizeWhichFits。

<code>    private int findLargestTextSizeWhichFits(RectF availableSpace) {
......
int bestSizeIndex = 0;
int lowIndex = bestSizeIndex + 1;
int highIndex = sizesCount - 1;
int sizeToTryIndex;
//這裡遍歷
while (lowIndex <= highIndex) {
sizeToTryIndex = (lowIndex + highIndex) / 2;
if (suggestedSizeFitsInSpace(mAutoSizeTextSizesInPx[sizeToTryIndex], availableSpace)) {
bestSizeIndex = lowIndex;
lowIndex = sizeToTryIndex + 1;
} else {
highIndex = sizeToTryIndex - 1;
bestSizeIndex = highIndex;
}

}
//返回指定索引的值
return mAutoSizeTextSizesInPx[bestSizeIndex];
}/<code>

這個方法其實就是從前面已經組織好的mAutoSizeTextSizesInPx數組中,遍歷,用每個數值通過suggestedSizeFitsInSpace來計算,這個值是否滿足要求。因為數組之前是被處理成有序的,所以這裡從中間開始向兩邊查找,類似二分法。

再看看按照指定字體大小計算是否滿足的代碼。原理就是通過TextView中處理文字的Layout來判斷這個字體是否滿足要求。

TextView內部的文字都是通過Layout處理的,有三種Layout:StaticLayout(默認使用的)、BoringLayout(單行顯示的)、DynamicLayoutSpan類型的)。

<code>private boolean suggestedSizeFitsInSpace(int suggestedSizeInPx, RectF availableSpace) {
//先讀取text內容

final CharSequence text = getText();
//讀取最大行數
final int maxLines = getMaxLines();
//用指定的字體大小,創建一個Paint
if (mTempTextPaint == null) {
mTempTextPaint = new TextPaint();
} else {
mTempTextPaint.reset();
}
mTempTextPaint.set(getPaint());
mTempTextPaint.setTextSize(suggestedSizeInPx);
/*TextView中的文字處理都是通過內部的Layout來處理的
*所以這裡通過一個臨時的layout,來判斷如果使用這個字體大小,會有多少行文字
*/
final StaticLayout.Builder layoutBuilder = StaticLayout.Builder.obtain(
text, 0, text.length(), mTempTextPaint, Math.round(availableSpace.right));

layoutBuilder.setAlignment(getLayoutAlignment())
.setLineSpacing(getLineSpacingExtra(), getLineSpacingMultiplier())
.setIncludePad(getIncludeFontPadding())
.setBreakStrategy(getBreakStrategy())
.setHyphenationFrequency(getHyphenationFrequency())
.setJustificationMode(getJustificationMode())
.setMaxLines(mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE)
.setTextDirection(getTextDirectionHeuristic());

final StaticLayout layout = layoutBuilder.build();

// 如果設置了最大行數,就返回false,就是不能設置行數的原因
if (maxLines != -1 && layout.getLineCount() > maxLines) {
return false;
}
......
return true;
}/<code>

suggestedSizeFitsInSpace找到符合要求的字體大小後,findLargestTextSizeWhichFits

的過程就算完成了。autoSizeText方法中最終會通過makeNewLayout,來創建一個新的Layout,這樣救護看到你要的效果啦。


分享到:


相關文章: