どうも。どっことです。今回は独自ViewとしてEditTextのカスタマイズクラスを実装したときにハマった問題について、備忘録を載せたいと思います。
Viewのコンストラクタとスタイル定義
ListView
などを使うために追加する独自View(①)と、TextView
などのコンポーネントのViewをカスタマイズするView(②)とで、特にコンストラクタの実装方針が異なります。今回はそんなコンストラクタの実装方針とそれに伴うスタイル定義について解説していきます。
コンストラクタについて
そもそもAndroidのView派生クラスには、3つのコンストラクタがあります。
1.Contextのみを引数とするコンストラクタ
constructor(context:Context)
これはコードからViewを生成するときに使われるコンストラクタです。LayoutInflater
を使わない場合は、このコンストラクタでViewを生成するコースがほとんどかと思います。属性値がほぼ初期値なので、ViewGroup
にaddView()
するときに自分で値を設定する必要があります。
2.Context, AttributeSetを引数とするコンストラクタ
constructor(context:Context, attrs:AttributeSet?)
これはxmlからViewを追加したときに呼ばれるコンストラクタです。
3.Context, AttributeSet, defStyleAttrを引数とするコンストラクタ
constructor(context:Context, attrs:AttributeSet?, defStyleAttr: Int)
これもxmlからView
を追加したときに呼ばれるコンストラクタです。「2.と何が違うの?」という疑問はAndroidアプリ開発者なら誰しも一度は思う謎ですが、それはxml内にstyle
が指定されているか否かの違いです。xml内でstyle
が指定されていれば3.、されていなければ2.が呼び出されます。
コンストラクタの実装方針について
それではコンストラクタを紹介したところで、先ほどの①と②での実装方針の違いについて説明していきます。
①の場合
気にしなくて問題ありません。以下のようなコードをコピペして使います。(以下はLinearLayout
の派生クラスとして定義した時の例です。)
class HogeView(context: Context, attrs: AttributeSet?, defStyleAttrs:Int) : LinearLayout(context, attrs, defStyleAttrs) {
constructor(context: Context): this(context, null, 0)
constructor(context: Context, attrs: AttributeSet?): this(context, attrs, 0)
// ...
}
②の場合
こちらは適切なスタイルの定義が必要になります。デフォルトの場合、例えばEditText
はこのような実装になっています。
public EditText(Context context) { // ①のコンストラクタ
this(context, null);
}
public EditText(Context context, AttributeSet attrs) { // ②のコンストラクタ
this(context, attrs, com.android.internal.R.attr.editTextStyle);
}
public EditText(Context context, AttributeSet attrs, int defStyleAttr) { // ③のコンストラクタ
this(context, attrs, defStyleAttr, 0);
}
第4引数については参考を見てもらうとして2.のコンストラクタが3.のコンストラクタをオーバーロードしているのは同じですが、引数が異なっています。①の場合は0
を渡していましたが、こちらはcom.android.internal.R.attr.editTextStyle
を渡しています。
EditTextStyle
にはEditText
が必要としている各属性の初期値が設定されており、これを渡してやらないと非常に残念な表示になってしまいます。(EditText
なのに、テキストが編集できないとか。1敗。)特に気にしないなら、これと同じように
com.android.internal.R.attr.editTextStyle
を渡してやれば問題ありませんが、「せっかく独自Viewを追加したのだから、スタイルもいい感じにしたい」ということであれば、追加で以下を実装する必要があります。(今回はEditText
を例として挙げます。)
実装手順
属性の追加
とりあえず何も考えずに attrs.xml
に以下を追加します。
名前も適当です。必要に応じて適切な名前にしてください。
<resources>
<attr name="hogeEditTextStyle" format="reference" />
</resources>
スタイルの追加
続いて何も考えずに styles.xml
に以下を追加する。
名前も...(略)
<resources>
<style name="HogeEditTextStyle" parent="android:Widget.EditText">
</style>
</resources>
テーマの追加
前項までに追加したものをつなぐために、themes.xml
に以下を追加します。
な...(略)
<style name="Theme.AppTheme" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- ... -->
<!-- Customize your theme here. -->
<item name="hogeEditTextStyle">@style/HogeEditTextStyle</item>
</style>
コンストラクタでの参照
最後に、「属性の追加」で追加したものを参照します。
class HogeEditText(context: Context, attrs: AttributeSet?, defStyleAttr: Int) :
AppCompatEditText(context, attrs, defStyleAttr) { // ③のコンストラクタ
constructor(context: Context) : this(context, null, R.attr.hogeEditTextStyle) // ①のコンストラクタ
constructor(context: Context, attrs: AttributeSet?) : this( // ②のコンストラクタ
context,
attrs,
R.attr.hogeEditTextStyle
)
//...
}
必要な実装は以上です。これでカスタムしたHogeEditText
でも残念な表示にならず、いい感じの表示とすることができます。
余談
第3引数が必要なコンストラクタの変数名が
defStyleAttr
となっており、これは default Style
Attributeを意味しているものと考えられます。つまり、デフォルトのStyleのattributeが設定されることを想定されているということです。xml上でstyleが設定されていなければ②の、styleが設定されていれば③のコンストラクタが呼ばれる点からもこの想定が正しいものと思われます。独自Viewとして想定しているデフォルトのスタイルなどは、①、②の場合に限らず、無理やりコード上で設定するのではなく今回紹介した方法のdefStyleAttr設定するのがスマートな実装方法のように感じられます。
まとめ
今回はコンストラクタと、カスタムView実装におけるスタイル定義について解説しました。カスタムViewの実装は、ソースコードの整理やロジックをまとめる際に非常に有効な手法です。今回のように、必要なコンストラクタがどれか、スタイル定義が必要か、などを理解した上で効率よく開発していきたいですね。