TextAppearance のプレビュー画面を作る

AndroidのリソースXMLを読み書きできない人に TextAppearance の定義内容を共有するためには、わかりやすく定義内容を一覧できる仕組みが必要です。 できれば、以下のように実際のアプリ内の表示とあわせて見られるのが一番良さそうです。

f:id:nein37:20170328151636p:plain

このプレビュー画面を作るためには「TextAppearane の一覧を取得する」「TextAppearane の定義内容を取得する」という処理が必要になるので、それぞれ方法を紹介します。

TextAppearane の一覧を取得する

TextAppearance や style は R.style. 以下に id が定数として定義されているので、これを利用してリフレクションで一覧を作ります。

今回は AppCompat の TextAppearance を対象にするので、TextAppearance_ の prefix を持つ style を対象にしました。

final List<Field> textAppearanceList = new ArrayList<>();
for (Field field : R.style.class.getFields()) {
    String fieldName = field.getName();
    if (fieldName.startsWith("TextAppearance_")) {
        textAppearanceList.add(field);
    }
}

これで R.style.TextAppearance_* の定数のリストができました。 ここで取得したリストではもともとxml に定義していた名前の ._ に変換されているので注意しましょう。

idの値は以下のどちらかの方法で取り出すことができます。

Field から直接値を取り出す

int styleId = 0;
try {
    styleId = field.getInt(null);
} catch (IllegalAccessException e) {
}

もとの名前から id を検索する

String styleName = field.getName().replace("_", ".");
int styleId = context.getResources().getIdentifier(styleName, "style", context.getContext().getPackageName());

TextAppearance の定義内容を知る

id がわかれば style の定義内容を取得することができます。

TypedArray typedArray = context.getTheme().obtainStyledAttributes(styleId, R.styleable.TextAppearance);

あとは TypedArray から値を取得するだけ… と思っていたら、TextView などが参照している com.android.internal.R.styleable.TextAppearance_textColor は外からアクセスできず、カスタムViewでもないので package.R.styleable にもなくてどうしよう、という感じでした。

悩んだ末、今回は作っていたのが preview ツールだったので TextViewに一度適用して TextView の setter から各デザイン情報を得るという乱暴な方法で実現しました。 TypedArray から直接属性を取る方法知りたい…

// 新しいAPIだが TextViewCompat 経由で setTextAppearance を呼べる
TextViewCompat.setTextAppearance(textView, styleId);

// textColor は ColorStateList だったりするが getCurrentTextColor が初期表示の色を取るのに便利
int textColor = (0xFFFFFFFF & textView.getCurrentTextColor());

// textSize は pixel で取れるので sp に変換する
// style定義時の値が dp か sp かを実行時に知る方法はない
float textSize = textView.getTextSize();
int textSizeSp = Math.round(textSize / context.getResources().getDisplayMetrics().scaledDensity);

// 太字/イタリック
boolean isBold = false;
boolean isItalic = false;
if (textView.getTypeface() != null) {
    isBold = textView,getTypeface().isBold();
    isItalic = textView.getTypeface().isItalic();
}

// ALLCAPS(後述)
boolean isAllCaps = false;
if (textView.getTransformationMethod() != null) {
    if (TextUtils.equals(textView.getTransformationMethod().getClass().getSimpleName(), "AllCapsTransformationMethod")) {
        isAllCaps = true;
    }
}

ALLCAPS 属性は set した瞬間に AllCapsTransformationMethod を TransformationMethod に set という乱暴な処理で扱われていて、isAllCaps() のような getter が存在しません。 しかも互換性のために AllCapsTransformationMethod が2種類(SDKとsupport)用意されていて、両方 @hide なので instanceof させることもできませんでした。 仕方がないのでクラス名だけの比較にしていますが、かなり厳しいですね…。

まとめ

かなり無理やりな処理ですが、一応なんとかなったのでサンプルとして公開しました。 アプリ内で TextAppearance に共通する prefix や suffix が決められていれば同じような処理で TextAppearance のプレビュー画面が作れると思います。

github.com