Building Custom Android Components

Androidにはアプリケーション構築に役立つ多くのビューコンポーネントが含まれています。例えばButtonTextViewEditTextListViewCheckBoxRadioButtonGallerySpinnerといったものです。またAutoCompleteTextViewImageSwitcherTextSwitcherといった高度で、特定用途向けのコンポーネントもあります。LinearLayoutFrameLayoutといった様々なレイアウトについても、ビュー構造を構成する要素と見ることが出来ます。

多くの場合はこれらのコンポーネントをうまく組み合わせて画面上に配置することで、やりたいことを実現出来ますが、ビューやレイアウトを拡張することで独自のコンポーネントを構築し、または継承を活用することで更に高度なものを構築することも出来ます。こうした要望は、以下のような場合に出てくるでしょう。

既存のビューを拡張したい理由は、きっとまだまだあるでしょう。このページでは、ビュー拡張のために何から手を付ければいいのか、という情報とそれを分かり易くするためのサンプルを記載します。

目次

基本アプローチ

以下のステップは、独自コンポーネントのためにまず知っておくべきことの概要です。

  1. 既存のViewクラスを継承し、サブクラスを作成します。
  2. 親クラスの一部メソッドのうち、先頭がonで始まるonDraw()onMeasure()onKeyDown()といったものをオーバーライドします。
  3. 実装が完了したら、拡張したクラスを利用します。拡張元のビューを利用していた場所を新たなクラスで置き換えることにより、拡張した機能の動作を確認出来ます。

拡張クラスをアクティビティクラスの内部クラスとして定義することも可能です。この場合アクティビティ側でビュークラス管理を行えるので便利ですが、独立したクラスと定義しても特に問題ありません。アプリケーション内で広く利用するためにパブリックなコンポーネントとしたい場合もあるでしょう。

完全カスタムコンポーネント

完全カスタムコンポーネントは開発者の思い通りに表示を行えるグラフィカルなコンポーネントを構築するのに利用できます。アナログゲージのように見えるグラフィカルなVUメータであったり、カラオケ風のテキスト見た目を実現するものであったり、という具合です。いずれにしても、標準コンポーネントをどのように組み合わせても構築できないようなもの、ということです。
幸い、簡単に思い通りの見た目表示と挙動を行うコンポーネントを構築することが出来ます(もっとも、開発者の想像力、画面サイズ、デスクトップ環境とは比べ物にならないくらい低い処理能力に制約されますが)。
完全カスタムコンポーネントは以下のようにして構築します。

onDraw()とonMeasure()

onDraw()で受け取れるCanvasには、2Dグラフィックス、他の標準のあるいはカスタムのコンポーネント、スタイルつきテキスト、その他諸々のものを表示することが出来ます。3Dグラフィックスを利用したい場合はViewクラスではなくGLViewを拡張する必要がありますが、概念は全く同じです。

onMeasure()のほうはもう少し複雑です。onMeasure()はコンポーネントとコンテナがやり取りを行う上で非常に重要なものです。onMeasure()には効率よくまた正確に、コンポーネント内のパーツ群の寸法を返すことが求められます。これは、親オブジェクト側から利用可能なサイズを引数として与えられ、計算結果をsetMeasuredDimension()メソッドの呼び出しにより返す必要があるため、更に複雑なものとなります。onMeasure()メソッド内からこのメソッドを呼べなかった場合、サイズ計測手続き内で例外が発生します。[※2]

onMeasure()の実装について高レベルな書き方をすると以下のようになります

  1. オーバーライドしたonMeasure()には幅・高さ制限が与えられます (int値のwidthMeasureSpecとheighMeasureSpec [※3] が使われます) 。これらでかけられる指定の一覧はView.onMeasure(int, int)にあります。このリファレンスは計測関連の役立つ情報が多く書かれているので一読の価値があります。
  2. コンポーネント内のonMeasure()メソッドでは、コンポーネント描画に必要な幅と高さを計算しなければなりません。極力与えられた幅・高さに合わせる必要がありますが、超えてしまうことも出来ます。この場合の描画方法は親オブジェクトに任されており、途中で表示を打ち切ったり、スクロール表示したり、例外を発生させたり、計測指定を変えて再度onMeasure()に問い合わせを行ったり、という具合の処理が考えられます。
  3. 幅と高さの計算が完了したら、計算結果をパラメータとしてsetMeasuredDimension(int width, int height)メソッドを呼ぶ必要があります。これを行わなければ例外が発生します。

完全カスタムコンポーネントの例

API DemosにあるCustomViewサンプルで、完全カスタムコンポーネントの例を示してあります。この完全カスタムコンポーネントはLabelViewクラスで定義されています。

LabelViewのサンプルには以下のような完全カスタムコンポーネントの様々な要素を盛り込んでいます。

LabelView完全カスタムコンポーネントの利用例は、サンプル内のcustom_view_1.xmlで確認出来ます。特に、android:名前空間とカスタムなapp:名前空間を併用している点に注目して下さい。app:系のパラメータはLabelViewが解釈して利用するためのもので、サンプルのRリソース定義クラス内の、スタイルをかけることの出来る内部クラスで定義されることになります。[※4]

コンパウンドコンポーネント、あるいはコンパウンドコントロール

いちから完全カスタムコンポーネントを作るのではなく、複数のコンポーネントを組み合わせて再利用可能なコンポーネントを作成したい場合には、コンパウンドコンポーネント(あるいはコンパウンドコントロール)を作成するのがよいでしょう。一言で説明してしまうと、これは複数のコントロールやビューをまとめて論理的にひとつのグループにまとめてしまうものです。例えば、コンボボックスは1行のEditTextとPopupListを組み合わせて表示したものと考えることが出来ます。ボタンを押してリスト内から何かを選択すると、その内容をEditTextのフィールドに表示します。一方でユーザは、EditTextの内容を自分で入力することも出来ます。

実は、この機能は既にAndroid上においてSpinnerAutoCompleteTextViewにより提供されていますしかしここでは分かり易い例としてコンボボックス構築について説明します。

コンパウンドコンポーネントの構築方法は以下のようになります

  1. 通常はレイアウトをかけるところから始めますので、まずはLayoutクラスを拡張します。コンボボックスの場合はLinearLayoutに水平設定をかけて利用することになるでしょう。異なるレイアウトをネストさせることが出来るので、コンパウンドコンポーネントは様々な形で複雑なものとしたり、構造化したりすることが出来ます。この際に、アクティビティの時と同様XMLを用いた定義型のアプローチとコードからプログラム的にネストさせて作成するアプローチのいずれかをとることが出来ます。
  2. 拡張クラスのコンストラクタでは親クラスが必要とする全パラメータを取得し、最初に親クラスのコンストラクタに受け渡す必要があります。その後に新コンポーネント特有のビュー初期化を行います。今回の例においては、この段階でEditTextのフィールドとPopupListを作成します。また、コンストラクタで利用する独自の属性やパラメータをXML内に盛り込むことが出来ます。
  3. また、内部のビューが生成するイベントを処理するリスナを作成する必要もあります。例えば、リスト内のアイテムがクリックされた際に、選択されたアイテムをEditText内に書き込む処理などです。
  4. 必要に応じてプロパティへのアクセッサを作成します。例えばEditTextの値をコンポーネント内で初期化したり、必要となった時に取得したり、という具合です。
  5. Layoutクラスを拡張する場合、onDraw()メソッドとonMeasure()メソッドはデフォルトのままでうまく動作するように実装されているので、開発者がオーバーライドする必要はありませんが、必要であればオーバーライドすることも出来ます。
  6. onKeyDown()など、他のon…系メソッドもオーバーライドすることが出来ます。例えば特定のキーが押された際に特定のデフォルト値を選択するように、という具合です。
要約すると、Layoutを使うメリットことで以下に挙げるものを始めとする多くのメリットを得られます。

コンパウンドコントロールの例

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だけでも読んでみてください。

いくつか注意点を記します

  1. 定義
    クラスは以下の行にて定義されています
    public static class MyEditText extends EditText
  2. クラスの初期化
    例によって最初にsuper()メソッドを呼びます。ちなみにこの時に呼び出すのはデフォルトのコンストラクタではなくパラメータを渡すタイプのものです。内容がXMLレイアウトファイルで指定されている場合、その情報をもとにEditTextが作成されますので、MyEditText側のコンストラクタでは両方のパラメータを取得してEditTextのコンストラクタに渡してやる必要があります。
  3. メソッドのオーバーライド
    この例ではonDraw()メソッドのみをオーバーライドしていますが、カスタムコンポーネントを作成する際には他のメソッドもオーバーライドする必要がある場合が多いでしょう。
    メモ帳のサンプルではonDraw()メソッドをオーバーライドし、onDraw()メソッドに引数として渡されるEditTextのビューキャンバス上に青い線を引いています。super.onDraw()メソッドはメソッドの終了前に呼び出しています。親クラスのメソッドはいずれにしても呼ぶ必要がありますが、この場合は必要な線を描画した後で呼び出しています。[※7]
  4. カスタムコンポーネントを利用する
    カスタムコンポーネントが完成しても使えなければ意味がありません。メモ帳の例では、カスタムコンポーネントを宣言レイアウトから直接利用しています。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" />
    

以上です。今回は単純な例を示しましたが、カスタムコンポーネントの作成は複雑なコンポーネントが欲しければ欲しいほど、当然ながら複雑なものとなります。

より洗練されたコンポーネントでは、より多くのon…系メソッドのオーバーライドが必要になることも考えられますし、独自のヘルパメソッド定義を追加したり、あるいはプロパティや挙動を変更したり、といった作業が必要になるかもしれません。いずれにしても、開発者の想像力とコンポーネントで何をしたいのかという目標さえあれば、どのようなものでも作ることが出来ます。

さらなる発展とコンポーネント化

ご覧のように、Androidは既存ビューのちょっとした変更に始まりコンパウンドコントロールや完全カスタマイズコンポーネントに至るあらゆるものを実現する洗練された強力なコンポーネントモデルを提供しています。これらのテクニックを組み合わせることで、思い通りの見た目を持つAndroidアプリケーションを構築出来るでしょう。