Building Custom Android Components
Androidにはアプリケーション構築に役立つ多くのビューコンポーネントが含まれています。例えばButton、TextView、EditText、ListView、CheckBox、RadioButton、Gallery、Spinnerといったものです。またAutoCompleteTextView、ImageSwitcher、TextSwitcherといった高度で、特定用途向けのコンポーネントもあります。LinearLayout、FrameLayoutといった様々なレイアウトについても、ビュー構造を構成する要素と見ることが出来ます。
多くの場合はこれらのコンポーネントをうまく組み合わせて画面上に配置することで、やりたいことを実現出来ますが、ビューやレイアウトを拡張することで独自のコンポーネントを構築し、または継承を活用することで更に高度なものを構築することも出来ます。こうした要望は、以下のような場合に出てくるでしょう。
- 2Dグラフィックスを利用して、アナログ電子機器に似せた音量調節ノブといった、自前レンダリングを行う新たなコンポーネントを作成したい場合
- コンボボックス(ポップアップリストと自由入力出来るテキストフィールドを組み合わせたもの)のようなものを作成したり、あるいは2ペイン選択コントロール(左右のペインにリストが振り分けられており、それを自分で選択して移動出来るようなもの)を作りたく、ビューコンポーネントをまとめて一つにしたい場合など
- 独自のレイアウトを作成したい場合。SDKに含まれているレイアウトはアプリケーション開発に十分なものですが、開発者によっては既存のものを拡張したい場合や、あるいは全く新たなものを作成したい場合があるでしょう。
- 既存コンポーネントの見た目や振る舞いを変更したい場合。例えば、EditTextコンポーネントの表示方法を変えたい場合など(メモ帳のサンプルではこれをメモ帳表示に罫線を引くのに利用しています)。
- キーが押された、といったイベントを取得して独自の処理を行いたい場合(例えばゲームにおいて)。
既存のビューを拡張したい理由は、きっとまだまだあるでしょう。このページでは、ビュー拡張のために何から手を付ければいいのか、という情報とそれを分かり易くするためのサンプルを記載します。
目次
基本アプローチ
以下のステップは、独自コンポーネントのためにまず知っておくべきことの概要です。
- 既存のViewクラスを継承し、サブクラスを作成します。
- 親クラスの一部メソッドのうち、先頭がonで始まるonDraw()、onMeasure()、onKeyDown()といったものをオーバーライドします。
- 実装が完了したら、拡張したクラスを利用します。拡張元のビューを利用していた場所を新たなクラスで置き換えることにより、拡張した機能の動作を確認出来ます。
拡張クラスをアクティビティクラスの内部クラスとして定義することも可能です。この場合アクティビティ側でビュークラス管理を行えるので便利ですが、独立したクラスと定義しても特に問題ありません。アプリケーション内で広く利用するためにパブリックなコンポーネントとしたい場合もあるでしょう。
完全カスタムコンポーネント
完全カスタムコンポーネントは開発者の思い通りに表示を行えるグラフィカルなコンポーネントを構築するのに利用できます。アナログゲージのように見えるグラフィカルなVUメータであったり、カラオケ風のテキスト見た目を実現するものであったり、という具合です。いずれにしても、標準コンポーネントをどのように組み合わせても構築できないようなもの、ということです。
幸い、簡単に思い通りの見た目表示と挙動を行うコンポーネントを構築することが出来ます(もっとも、開発者の想像力、画面サイズ、デスクトップ環境とは比べ物にならないくらい低い処理能力に制約されますが)。
完全カスタムコンポーネントは以下のようにして構築します。
- ご想像の通り、ベースにすべき最も一般的なクラスはViewなので、作業は通常このクラスを拡張することから始まります。
- XMLから属性やパラメータを受け取るコンストラクタを作成することが出来、これに独自の属性やパラメータを取らせるようにも出来ます。VUメータの例では、色やメータ幅、ニードルの幅といったものです[※1] 。
- 必要に応じて独自のイベントリスナ、プロパティアクセッサといったものを作成します
- だいたいの場合、表示を行うためのonMeasure()をオーバーライドしたり、onDraw()をオーバーライドしたりすることになるでしょう。共にデフォルトの挙動を持ちますが、デフォルトのonDraw()は何もせず、onMeasure()も100x100という適当な値を設定するのみです。
- 他のon…系メソッドも必要に応じてオーバーライドします。
onDraw()とonMeasure()
onDraw()で受け取れるCanvasには、2Dグラフィックス、他の標準のあるいはカスタムのコンポーネント、スタイルつきテキスト、その他諸々のものを表示することが出来ます。3Dグラフィックスを利用したい場合はViewクラスではなくGLViewを拡張する必要がありますが、概念は全く同じです。
onMeasure()のほうはもう少し複雑です。onMeasure()はコンポーネントとコンテナがやり取りを行う上で非常に重要なものです。onMeasure()には効率よくまた正確に、コンポーネント内のパーツ群の寸法を返すことが求められます。これは、親オブジェクト側から利用可能なサイズを引数として与えられ、計算結果をsetMeasuredDimension()メソッドの呼び出しにより返す必要があるため、更に複雑なものとなります。onMeasure()メソッド内からこのメソッドを呼べなかった場合、サイズ計測手続き内で例外が発生します。[※2]
onMeasure()の実装について高レベルな書き方をすると以下のようになります
- オーバーライドしたonMeasure()には幅・高さ制限が与えられます (int値のwidthMeasureSpecとheighMeasureSpec [※3] が使われます) 。これらでかけられる指定の一覧はView.onMeasure(int, int)にあります。このリファレンスは計測関連の役立つ情報が多く書かれているので一読の価値があります。
- コンポーネント内のonMeasure()メソッドでは、コンポーネント描画に必要な幅と高さを計算しなければなりません。極力与えられた幅・高さに合わせる必要がありますが、超えてしまうことも出来ます。この場合の描画方法は親オブジェクトに任されており、途中で表示を打ち切ったり、スクロール表示したり、例外を発生させたり、計測指定を変えて再度onMeasure()に問い合わせを行ったり、という具合の処理が考えられます。
- 幅と高さの計算が完了したら、計算結果をパラメータとしてsetMeasuredDimension(int width, int height)メソッドを呼ぶ必要があります。これを行わなければ例外が発生します。
完全カスタムコンポーネントの例
API DemosにあるCustomViewサンプルで、完全カスタムコンポーネントの例を示してあります。この完全カスタムコンポーネントはLabelViewクラスで定義されています。
LabelViewのサンプルには以下のような完全カスタムコンポーネントの様々な要素を盛り込んでいます。
- Viewクラスを拡張し、完全カスタムコンポーネント作成を行っています。
- XML内で定義されたビューパラメータを受け取るコンストラクタを定義しています。あるものは親クラスであるViewに受け渡していますが、より重要なのはLabelViewで利用するいくつかの独自属性が定義されているという点です。
- ラベルコンポーネントで必要と思われるsetText()、setTextSize()、setTextColor()といったパブリックなメソッドを定義しています。
- コンポーネントの表示サイズを決定するためにonMeasureメソッドをオーバーライドしています。ちなみに実装はプライベートメソッドのmeasureWidth()で行っています。
- 与えられたキャンバスに対してラベルを描画するために、onDraw()メソッドをオーバーライドしています。
LabelView完全カスタムコンポーネントの利用例は、サンプル内のcustom_view_1.xmlで確認出来ます。特に、android:名前空間とカスタムなapp:名前空間を併用している点に注目して下さい。app:系のパラメータはLabelViewが解釈して利用するためのもので、サンプルのRリソース定義クラス内の、スタイルをかけることの出来る内部クラスで定義されることになります。[※4]
コンパウンドコンポーネント、あるいはコンパウンドコントロール
いちから完全カスタムコンポーネントを作るのではなく、複数のコンポーネントを組み合わせて再利用可能なコンポーネントを作成したい場合には、コンパウンドコンポーネント(あるいはコンパウンドコントロール)を作成するのがよいでしょう。一言で説明してしまうと、これは複数のコントロールやビューをまとめて論理的にひとつのグループにまとめてしまうものです。例えば、コンボボックスは1行のEditTextとPopupListを組み合わせて表示したものと考えることが出来ます。ボタンを押してリスト内から何かを選択すると、その内容をEditTextのフィールドに表示します。一方でユーザは、EditTextの内容を自分で入力することも出来ます。
実は、この機能は既にAndroid上においてSpinnerとAutoCompleteTextViewにより提供されていますしかしここでは分かり易い例としてコンボボックス構築について説明します。
コンパウンドコンポーネントの構築方法は以下のようになります
- 通常はレイアウトをかけるところから始めますので、まずはLayoutクラスを拡張します。コンボボックスの場合はLinearLayoutに水平設定をかけて利用することになるでしょう。異なるレイアウトをネストさせることが出来るので、コンパウンドコンポーネントは様々な形で複雑なものとしたり、構造化したりすることが出来ます。この際に、アクティビティの時と同様XMLを用いた定義型のアプローチとコードからプログラム的にネストさせて作成するアプローチのいずれかをとることが出来ます。
- 拡張クラスのコンストラクタでは親クラスが必要とする全パラメータを取得し、最初に親クラスのコンストラクタに受け渡す必要があります。その後に新コンポーネント特有のビュー初期化を行います。今回の例においては、この段階でEditTextのフィールドとPopupListを作成します。また、コンストラクタで利用する独自の属性やパラメータをXML内に盛り込むことが出来ます。
- また、内部のビューが生成するイベントを処理するリスナを作成する必要もあります。例えば、リスト内のアイテムがクリックされた際に、選択されたアイテムをEditText内に書き込む処理などです。
- 必要に応じてプロパティへのアクセッサを作成します。例えばEditTextの値をコンポーネント内で初期化したり、必要となった時に取得したり、という具合です。
- Layoutクラスを拡張する場合、onDraw()メソッドとonMeasure()メソッドはデフォルトのままでうまく動作するように実装されているので、開発者がオーバーライドする必要はありませんが、必要であればオーバーライドすることも出来ます。
- onKeyDown()など、他のon…系メソッドもオーバーライドすることが出来ます。例えば特定のキーが押された際に特定のデフォルト値を選択するように、という具合です。
要約すると、Layoutを使うメリットことで以下に挙げるものを始めとする多くのメリットを得られます。
- アクティビティ画面と同様にしてXMLでレイアウト指定を行う方法とビューをプログラム的に作成してレイアウト内にネストさせることが出来ます。[※5]
- onDraw()やonMeasure()といったon…系のメソッドをオーバーライドせずにうまく動作することが多いです。
- また、迅速に複雑な独自コンパウンドビューを作成し、まるで単一コンポーネントのようにして再利用することが出来ます。
コンパウンドコントロールの例
SDKに含まれるAPIデモプロジェクトに、2つのリストデモが入っています。Views/Listsディレクトリ内のExample 4とExample 6では、LinearLayoutを拡張したSpeechViewを作成し、スピーチの引用を表示する例が使われています。対応するサンプルコード内のクラスはList4.javaとList6.javaです。
既存のコンポーネントをいじる
カスタムコンポーネント作成において、場合によってはより簡単な方法を利用することが出来ます。必要なコンポーネントに良く似たものがある場合は当該コンポーネントを拡張し、自分の好きなように挙動を変更するというものです。完全なカスタムコンポーネントと全く同じことが実現できますが、View階層のうちで用途が限定されたものをベースにすることにより既に実装されている挙動をうまく活用することが出来ます。
例えばSDKのサンプルにあるNotePad applicationにはEditTextを拡張して罫線付きのメモ帳を作成するといったテクニックを含む、Androidプラットフォームの使い方に関する様々なデモが含まれています。これは完璧な例ではなくAPIについても現状の早期プレビュー版から変更される可能性もありますが、本質をついたデモと言えます。
もしまだメモ帳のサンプルを見ていないようでしたら、Eclipseにプロジェクトをインポートするか、NoteEditor.java内のMyEditTextだけでも読んでみてください。
いくつか注意点を記します
- 定義
クラスは以下の行にて定義されています
public static class MyEditText extends EditText
- NoteEditorアクティビティの内部クラスとして定義されていますがパブリック指定がされているため、必要であればNoteEditorクラス外からNoteEditor.MyEditTextとしてアクセスすることが出来ます。
- スタティッククラスなので、親クラスからデータアクセスを行う為のいわゆるシンセティックメソッド[※6] を持ちません。逆に言えば、NoteEditorとはあまり強固なつながりを持っていないということです。このような内部クラスの作成は外側のクラスから状態チェックを行う必要が無い場合に、クラスをコンパクトにまとめかつ他のクラスから利用しやすくするためのテクニックです。
- このクラスは、この例においてカスタマイズすることを決めたEditTextを拡張して作られています。そのため実装が完了したら通常のEditTextビューのように扱うことが出来るようになります。
- クラスの初期化
例によって最初にsuper()メソッドを呼びます。ちなみにこの時に呼び出すのはデフォルトのコンストラクタではなくパラメータを渡すタイプのものです。内容がXMLレイアウトファイルで指定されている場合、その情報をもとにEditTextが作成されますので、MyEditText側のコンストラクタでは両方のパラメータを取得してEditTextのコンストラクタに渡してやる必要があります。
- メソッドのオーバーライド
この例ではonDraw()メソッドのみをオーバーライドしていますが、カスタムコンポーネントを作成する際には他のメソッドもオーバーライドする必要がある場合が多いでしょう。
メモ帳のサンプルではonDraw()メソッドをオーバーライドし、onDraw()メソッドに引数として渡されるEditTextのビューキャンバス上に青い線を引いています。super.onDraw()メソッドはメソッドの終了前に呼び出しています。親クラスのメソッドはいずれにしても呼ぶ必要がありますが、この場合は必要な線を描画した後で呼び出しています。[※7]
- カスタムコンポーネントを利用する
カスタムコンポーネントが完成しても使えなければ意味がありません。メモ帳の例では、カスタムコンポーネントを宣言レイアウトから直接利用しています。res/layoutフォルダ内のnote_editor.xmlを見てみましょう。
<view xmlns:android="http://schemas.android.com/apk/res/android"
class="com.google.android.notepad.NoteEditor$MyEditText"
id="@+id/note"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="@android:drawable/empty"
android:padding="10dip"
android:scrollbars="vertical"
android:fadingEdge="vertical" />
- カスタムコンポーネントはXML上で通常のビューと同様に作成され、完全なパッケージ名で指定されています。また、定義した内部クラスがJavaプログラミング言語で一般的な、NoteEditor$MyEditTextという書式で指定されていることに注意して下さい。
- 定義内で指定されている他の属性やパラメータはカスタムコンポーネントのコンストラクタ経由でEditTextに渡されるため、ここでの指定はEditTextを利用する際と同じものになっています。ここでは以下でまた触れるように独自のパラメータを指定することも出来ます。
以上です。今回は単純な例を示しましたが、カスタムコンポーネントの作成は複雑なコンポーネントが欲しければ欲しいほど、当然ながら複雑なものとなります。
より洗練されたコンポーネントでは、より多くのon…系メソッドのオーバーライドが必要になることも考えられますし、独自のヘルパメソッド定義を追加したり、あるいはプロパティや挙動を変更したり、といった作業が必要になるかもしれません。いずれにしても、開発者の想像力とコンポーネントで何をしたいのかという目標さえあれば、どのようなものでも作ることが出来ます。
さらなる発展とコンポーネント化
ご覧のように、Androidは既存ビューのちょっとした変更に始まりコンパウンドコントロールや完全カスタマイズコンポーネントに至るあらゆるものを実現する洗練された強力なコンポーネントモデルを提供しています。これらのテクニックを組み合わせることで、思い通りの見た目を持つAndroidアプリケーションを構築出来るでしょう。
- [※1]dampingが抜けた
- [※2]もっとこなれた訳に
- [※3]元版、heightの間違い?
- [※4]おかしな訳なので、実際のサンプルを見てから訳しなおす
- [※5]今ひとつ伝わりにくそうなので再検討
- [※6]一般的な訳語はある?
- [※7]冗長