TextViewのAutoSizeの実装をみてみた
この記事はFOLIO Advent Calendarの10日目の記事です。
先日アプリ内で、TextViewのAutoSizeを使用する機会があり、どういう実装になっているのかが気になったので、アドベントカレンダーの季節ということもあったので、実際に見てみることにしました!
(実際に見てみたところ実装が複雑ではなかったのでかなり短い内容になりましたが、、、)
AutoSizeとは
まずは、TextViewのAutoSizeについて、簡単に確認していこうと思います。
TextViewのAutoSizeは、TextViewの幅、高さ、文字列の長さに応じて、自動でTextSizeを調整してくれる機能です。
AutoSizeに必要な設定は、以下4つの要素になります。
autoSizePresetSizesという、予め決めたサイズでTextSize調整してくるものもありますが、今回はとりあえず決めた範囲内で指定したステップごとにサイズを変更してくれる以下の設定方法をメインに扱います。
android:autoSizeMinTextSize
自動調整される際の最小TextSize
android:autoSizeMaxTextSize
自動調整される際の最大TextSize
android:autoSizeStepGranularity
ここに設定したTextSizeごとに大きくしたり小さくしたりする
android:autoSizeTextType
uniform
を設定すると自動調整をしてくれる, none
を設定すると自動調整をしない
実際に設定してみると、↓のような動きになります。
GIF載せれたら載せたい
上記4つの要素を設定するだけで、TextViewのAutoSize自体は簡単に設定することができます。
AutoSizeの内部実装
ここから本題ですが、AutoSizeの内部実装はどのようになっているかを見てみたいと思います。
TextViewクラスからたどってみたところAppCompatTextViewAutoSizeHelper
というクラスがありました。
このクラスのコメントには、
Utility class which encapsulates the logic for the TextView auto-size text feature
と記載がありました。
このクラスでAutoSize関連の処理をしているようなので、このクラスの内容をみてみれば、どのような実装をしているかわかりそうです。
setupAutoSizeText()
AppCompatTextViewAutoSizeHelper
をザッと見てみたところ、setupAutoSizeText()
といういかにもセットアップを行っていそうなメソッドがあったので、まずはここから見てみます。
private boolean setupAutoSizeText() {
if (supportsAutoSizeText()
&& mAutoSizeTextType == TextViewCompat.AUTO_SIZE_TEXT_TYPE_UNIFORM) {
// Calculate the sizes set based on minimum size, maximum size and step size if we do
// not have a predefined set of sizes or if the current sizes array is empty.
if (!mHasPresetAutoSizeValues || mAutoSizeTextSizesInPx.length == 0) {
// Calculate sizes to choose from based on the current auto-size configuration.
final int autoSizeValuesLength = ((int) Math.floor((mAutoSizeMaxTextSizeInPx
- mAutoSizeMinTextSizeInPx) / mAutoSizeStepGranularityInPx)) + 1;
final int[] autoSizeTextSizesInPx = new int[autoSizeValuesLength];
for (int i = 0; i < autoSizeValuesLength; i++) {
autoSizeTextSizesInPx[i] = Math.round(
mAutoSizeMinTextSizeInPx + (i * mAutoSizeStepGranularityInPx));
}
mAutoSizeTextSizesInPx = cleanupAutoSizePresetSizes(autoSizeTextSizesInPx);
}
mNeedsAutoSizeText = true;
} else {
mNeedsAutoSizeText = false;
}
return mNeedsAutoSizeText;
}
まず設定されたautoSizeMaxText
とautoSizeMinTextSize
の差をautoSizeStepGranularity
で割り、まずは設定できるTextSizeの個数を計算しています。
そして、autoSizeMinTextSize
を初期値としてautoSizeStepGranularity
を順に足していくことで、設定できる範囲のTextSizeをすべて配列に格納しているようです。
その後、計算した値をcleanupAutoSizePresetSizes
に渡して、値がPositiveか、重複している値がないかをチェックしています。
そして、計算した値をmAutoSizeTextSizesInPx
に入れて保持しているようです。
なので、AutoSizeのセットアップとしては、単純に設定できる範囲のTextSizeをすべて計算して保持しておく処理を行っているようです。
autoSizeText()
次は本命っぽいautoSizeText
というメソッドを見ていきます。
void autoSizeText() {
if (!isAutoSizeEnabled()) {
return;
}
if (mNeedsAutoSizeText) {
if (mTextView.getMeasuredHeight() <= 0 || mTextView.getMeasuredWidth() <= 0) {
return;
}
final boolean horizontallyScrolling = mImpl.isHorizontallyScrollable(mTextView);
final int availableWidth = horizontallyScrolling
? VERY_WIDE
: mTextView.getMeasuredWidth() - mTextView.getTotalPaddingLeft()
- mTextView.getTotalPaddingRight();
final int availableHeight = mTextView.getHeight() - mTextView.getCompoundPaddingBottom()
- mTextView.getCompoundPaddingTop();
if (availableWidth <= 0 || availableHeight <= 0) {
return;
}
synchronized (TEMP_RECTF) {
TEMP_RECTF.setEmpty();
TEMP_RECTF.right = availableWidth;
TEMP_RECTF.bottom = availableHeight;
final float optimalTextSize = findLargestTextSizeWhichFits(TEMP_RECTF);
if (optimalTextSize != mTextView.getTextSize()) {
setTextSizeInternal(TypedValue.COMPLEX_UNIT_PX, optimalTextSize);
}
}
}
// 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;
}
ここでは、最初にAutoSizeが設定されているTextViewのwidthとheightを取得しているようです。取得したTextViewのサイズはfindLargestTextSizeWhichFits
というメソッドに渡されます。
private int findLargestTextSizeWhichFits(RectF availableSpace) {
final int sizesCount = mAutoSizeTextSizesInPx.length;
if (sizesCount == 0) {
throw new IllegalStateException("No available text sizes to choose from.");
}
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];
}
そして、findLargestTextSizeWhichFits
では、TextViewに収まる最大のTextSizeを計算しています。
内容としては、lowIndex
とhighIndex
を用意して、lowIndexには
初期値として、1
を, highIndex
には、setupAutoSizeText
で計算したmAutoSizeTextSizesInPx
のlength - 1(要するに最大TextSizeのIndex)を代入しています。
そして、lowIndex
とhighIndex
を足して2で割ったIndexのサイズをsuggestedSizeFitsInSpace
に渡し、サイズが大きいか、小さいかを判定していきます。
サイズが大きい(TextViewからはみ出す)場合 -> highIndex
に現在試しているサイズのIndex - 1に更新して、bestSizeIndex
にその値を保持する。
サイズが小さい(TextViewに収まっている)場合 -> bestSizeIndex
に現在のlowIndex
を保持し、lowIndex
を現在試しているサイズのIndex + 1に更新します。
これを、lowIndex <= highIndex
が成り立っている間、繰り返します。
例.
mAutoSizeTextSizesInPx = [1, 2, 3, 4]
mAutoSizeTextSizesInPx.length = 4
の場合、
lowIndex = 1
highIndex = 3
でスタート。
(1 + 3) / 2 = 2
TextViewに収まる場合
lowIndex = 2 + 1 = 3
highIndex = 3
=> 収まる場合TextSizeは4(mAutoSizeTextSizesInPx[3])
=> 収まらない場合TextSizeは3(mAutoSizeTextSizesInPx[2])
TextViewに収まらない場合
highIndex = 2 - 1 = 1
lowIndex = 1
=> 収まる場合TextSizeは2(mAutoSizeTextSizesInPx[1])
=> 収まらない場合TextSizeは1(mAutoSizeTextSizesInPx[0])
というようにTextSizeを判定していって、AutoSizeを実現しているようです。
AutoSizeが実行されるタイミング
AutoSizeがどのようにTextSizeを調整しているかはわかったので、最後にどのタイミングでAutoSizeが実行されるかを確認したいと思います。
autoSizeText()
から参照元をさかのぼって見たところ、AppCompatTextView#onTextChanged()
にたどり着きました。
TextViewにセットされている文字列が変わったタイミングで、毎回AutoSizeが実行されているようです。
まとめ
AppCompatTextViewAutoSizeHelper
には、もちろん今回見たもの以外にも、たくさんのメソッド、処理があるのですが、今回は基本の部分だけ見てみたところ、意外とシンプルに実装されていて驚きました。
そして、AutoSizeが実行されるタイミングを調べているときに気がついたのですが、ButtonでもautoSize使えるようですね。(ButtonでAutoSizeを使うケースなかなかないと思いますが)
元々実装を見てみたいと思ったきっかけは、LinearLayoutで横並びに置いているTextViewを組み合わせてAutoSizeしてくれないかなぁと思ったところがきっかけでしたが、今回実装を見てみたところカスタマイズしたりすることはできそうだなぁと思いました。
横並びのTextViewを両方AutoSizeできるようにしたり、高さにwrap_content
を指定できるようにするなどにも挑戦してみたいと思ったので、今度やってみようと思います!
Discussion