Edited at

AndroidのStyleとかThemeとかAttrとかStylableとか。

More than 1 year has passed since last update.

整理しておかないと忘れてしまうので。


Style

例えばtextColortextSizeをXMLで指定する場合、XMLでView毎に同じ指定を書くのではなく、Styleリソースに定義することで共通化することができる。

Styleで指定した属性と、レイアウトXMLで指定した属性が重複する場合、レイアウトで指定した値が優先される。

どのViewがどのXML属性に対応しているのかは、リファレンスを読めば分かる。


実際の処理

これがどういう処理になっているのかというと、Viewクラスのコンストラクタのうち、

public View(Context context, AttributeSet attrs)

が呼び出され、Styleリソースで指定された値がAttributeSetの中に、TypedValueオブジェクトとして格納される。

public View(Context context, AttributeSet attrs, int defStyle)

が呼びされると思い込むのは良くある勘違いである。

XMLでStyleを適用させることが非常に便利で簡単なことに反して、 コードでStyleリソースを直接指定する方法は存在しない


Theme

ActivityやApplicationのスコープで見た目を共通化するのがThemeである。

非常に紛らわしいことに、Styleリソースが利用される。

Themeの属性はactionBarStyleなどが定義されている。

属性の一覧は、以下に記載されているが、これまた非常に紛らわしいことにlayout_widthのようなViewの属性と並列して記載されている。

加えて、どの属性に何の意味があり、どのような値を指定すればいいのかについて、ドキュメント化されていないため、Theme.Holoなどを見ながらなんとなく分かったような気になるしかない気がする。


テーマがどうViewに渡されるのか

Themeによる指定は、contextによって渡される。

Activityはandroid.view.ContextThemeWrapperを継承しているため、そのActivityでnewinflateした全てのViewにはThemeが伝播するという仕組みである。

Resources.ThemeresolveAttribute()で、contextからTypedValueとしてテーマの値を取得できる。

例外的に、AlertDialogAlertDialog.Builderは、Theme適用のスコープをApplication単位にしたいためか、特別な処理となっている。

APIレベルごとの差もあり非常に厄介なので、DialogのThemeについて完全に理解するより、setView()を使ってカスタムダイアログを作る方が安全かと思われる。


Attr

Styleも紛らわしいけど、Attrは更にいろいろ意味があって分かりにくい。


属性名

先に書いたとおり、ViewのtextColorなどのXML属性、ThemeのためのtextColorPrimary属性を定義しているのがR.attrである。


属性リソース

レイアウトのXMLを作成するとき、layout_widthであればwrap_contentmatch_parentのような属性値から選択することができるが、これを自作することもできる。

<attr name="direction" format="integer">

<enum name="top" value="0" />
<enum name="bottom" value="1" />
</attr>

例えばこのように書けば、direction属性をXMLで指定するときに、自動的に候補として出てくるため、非常に便利。

formatにはintやfloat、colorのほか、ビットフラグや参照(主にStyleリソース)も指定できる。

もっともカスタムビューを作らないと有難味は分からない。


属性参照

テーマに指定されている属性の値を参照するには、属性参照(と一般に呼ばれているのか知らないけど)の記法を利用する。

android:textColor="?android:textColorSecondary"

と書くことで、現在のテーマに指定されている、android:textColorSecondaryの値が設定される。

android:textColor="@android:color/secondary_text_dark"

このように直接色を指定したり、Styleリソースを適用するよりも一手間増えるが、Themeに応じて値が変わるためより柔軟な記述となる。

利用用途としては、抽象テーマを作りたい場合などである。

例えば、android:textAppearanceandroid:textColorPrimaryを属性参照しているが、これはTheme.Holoにて属性参照で値を参照しておいて、Theme.Holo.WhiteTheme.Holo.Blackandroid:textColorPrimaryだけ変えることで、それぞれのテーマでandroid:textAppearanceを再指定することなく、文字色だけを変更しているのである。


Styleable

カスタムなThemeやView属性を作成するには、declare-styleable要素を利用する。

<?xml version="1.0" encoding="utf-8"?>

<resources>

<!-- カスタムビュー属性 -->
<declare-styleable name="CustomView">
<!-- 既に存在するdirectionを参照するときには、format指定をしない -->
<attr name="direction" />
</declare-styleable>

<!-- カスタムテーマ属性、nameの指定が微妙な気がする -->
<declare-styleable name="customAttribute">
<attr name="customAttribute" format="color" />
</declare-styleable>

</resources>

すると、R.styleableに対応する値が生成されて、利用可となる。


カスタムテーマ属性の使い方

例えばここではformatとしてcolorを指定したテーマ属性を作成したので、以下のように色指定する。(Styleリソースを渡したい場合は、formatとしてreferenceを指定すればよい)

<style name="AppBaseTheme" parent="Theme.AppCompat.Light">

<item name="customAttribute">@color/hogeColor</item>
</style>

レイアウトから参照するには、属性参照を使う。

android:background="?attr/customAttribute"

これを応用することで、Styleリソースをコードで渡すことができる。Viewコンストラクタの第三引数に属性名を渡せば良い。(逆に言えば、カスタムテーマ属性をサポートしない限り、Viewの引数3つのコンストラクタを実装する意義はない)


カスタムビュー属性の使い方

前提としてカスタムビューの利用法を理解する必要があるけど、それはスルーしておき。カスタムビュー属性を使うにはまずスキーマ宣言を書く必要がある。スキーマ宣言はルート要素でしか行えない。

<your.package.CustomView

xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:custom="http://schemas.android.com/apk/res/your.package"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
custom:direction="top" />

declare-styleableで指定したnameと、ビュー名は一致させる必要がある。また、カスタム属性値を使いたい場合、以下のようにも書ける。

<!-- カスタムビュー属性 -->

<declare-styleable name="CustomView">
<attr name="direction" format="integer">
<enum name="top" value="0" />
<enum name="bottom" value="1" />
</attr>
</declare-styleable>

値を取得するにはカスタムビューのコンストラクタでAttributeSetから取り出す。

このとき、属性名の解決にR.styleableを利用する。

public CustomView(Context context, AttributeSet attrs){

super(context, attrs);
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CustomView);
int direction = array.getInt(R.styleable.CustomView_direction, 0);
array.recycle();
}