カスタムビュー コンポーネント

Android には、UI 作成のための高度で強力なコンポーネント化モデルが用意されています。ベースになるのは基本的なレイアウト クラス ViewViewGroup です。プラットフォームには、ビルド済みのさまざまな View サブクラスと ViewGroup サブクラス(ウィジェットおよびレイアウトと呼ばれます)が組み込まれており、UI の作成に利用できます。

利用可能なウィジェットとしては、ButtonTextViewEditTextListViewCheckBoxRadioButtonGallerySpinner などがあり、特別な用途を持つ AutoCompleteTextViewImageSwitcherTextSwitcher などもあります。

利用可能なレイアウトとしては、LinearLayoutFrameLayoutRelativeLayout などがあります。他の例については、一般的なレイアウト オブジェクトをご覧ください。

ビルド済みのウィジェットまたはレイアウトの中に自分の用途に適したものがない場合は、独自の View サブクラスを作成できます。既存のウィジェットまたはレイアウトを少し調整するだけでよい場合は、対象のウィジェットまたはレイアウトをサブクラス化して、そのメソッドをオーバーライドできます。

独自の View サブクラスを作成すると、画面要素の外観と機能をきめ細かく制御できます。コントロールでカスタムビューを利用することで、以下のような機能を実現できます。

  • フルカスタム レンダリングのビュータイプを作成できます。たとえば、2D グラフィックを使用して、アナログ電子制御装置のような「音量調節」つまみを表示できます。
  • 複数の View コンポーネントを組み合わせて、新しい単一のコンポーネントにまとめることができます。たとえば、コンボボックス(テキストを自由に入力できるフィールドとポップアップ リストを組み合わせたもの)や、デュアルペイン セレクタ コントロール(左右のペインにそれぞれリストがあり、リスト項目を再配置できるもの)を作成できます。
  • 画面上の EditText コンポーネントのレンダリング方法をオーバーライドできます(NotePad チュートリアルでは、この手法を利用して、罫線付きのメモ帳を作成しています)。
  • キーの押下などの他のイベントをキャプチャして、独自の方法で処理できます(ゲームなどに利用できます)。

以下のセクションでは、カスタムビューを作成する方法と、それをアプリ内で利用する方法について説明します。 詳細については、View クラスをご覧ください。

基本的なアプローチ

独自の View コンポーネントの作成を開始する際に必要となる知識の概要を次に示します。

  1. 既存の View クラスまたはサブクラスを、独自のクラスを使用して拡張します。
  2. スーパークラスの一部のメソッドをオーバーライドします。オーバーライドの対象となるスーパークラス メソッドは、先頭に「on」が付いているものです(onDraw()onMeasure()onKeyDown() など)。 これは、ライフサイクルなどの機能フックの際に、Activity または ListActivityon... イベントをオーバーライドするのと同様です。
  3. 新しい拡張クラスを使用します。完成した新しい拡張クラスは、ベースとなったビューの代わりとして使用できます。

ヒント: 拡張クラスは、それを使用するアクティビティの内部クラスとして定義できます。この手法は、拡張クラスへのアクセスを制御できるので便利ですが、必須ではありません(新しいパブリック ビューを作成すると、アプリでより幅広い用途に利用できます)。

フルカスタム コンポーネント

フルカスタム コンポーネントを使用すると、望みどおりのグラフィカル コンポーネントを作成できます。たとえば、アナログ機器のような外観の音量メーターや、カラオケで歌うときにボールがバウンドしながら歌詞に合わせて移動する字幕用テキストビューを作成できます。どちらの例も、組込みコンポーネントを組み合わせるだけでは実現できません。

しかし、想像力を働かせて、画面のサイズと利用可能な処理能力を考慮すれば、思いどおりの外観と機能を備えたコンポーネントを簡単に作成できます(処理能力については、最終的にアプリが実行されるのは開発用のデスクトップ ワークステーションより大幅に処理能力が低いデバイスであることに注意する必要があります)。

フルカスタム コンポーネントを作成するには:

  1. 拡張可能なビューの中で最も汎用性があるのは、当然ながら View です。したがって、通常はこれを拡張して新しいスーパー コンポーネントを作成することから始めます。
  2. 属性とパラメータを XML から受け取ることができるコンストラクタを作成することも、独自の属性とパラメータを利用することもできます(たとえば、音量メーターの色と範囲、メーター針の幅と減衰量などを指定できます)。
  3. 一般的には、独自のイベント リスナー、プロパティ アクセサ、修飾子を作成し、コンポーネント クラスに高度な動作を組み込みます。
  4. ほとんどの場合は onMeasure() をオーバーライドします。コンポーネントで何かを表示する場合は、おそらく onDraw() もオーバーライドする必要があります。これらのメソッドには、ともにデフォルト動作があります。デフォルトの onDraw() は何もしません。デフォルトの onMeasure() は常に 100 × 100 のサイズを設定しますが、このサイズがニーズを満たすことはあまりないと思われます。
  5. 必要に応じて他の on... メソッドもオーバーライドできます。

onDraw()onMeasure() を拡張する

onDraw() メソッドは Canvas を備えています。キャンバスの上に、2D グラフィック、各種の標準コンポーネントまたはカスタム コンポーネント、スタイル付きテキストなど、必要なものをすべて実装できます。

注: ただし、3D グラフィックは除きます。3D グラフィックを使用する場合は、View ではなく SurfaceView を拡張して、別のスレッドから描画する必要があります。詳細については、GLSurfaceViewActivity サンプルをご覧ください。

onMeasure() の場合はもう少し複雑です。onMeasure() は、コンポーネントとそのコンテナとの間のレンダリング コントラクトにとって欠かせない要素です。内部パーツの測定値を効率的かつ正確にレポートするには、onMeasure() をオーバーライドする必要があります。また、親からの制限に基づく要件(onMeasure() メソッドに渡します)と、setMeasuredDimension() メソッドを呼び出すための要件(計算された後の幅と高さの測定値を指定します)があるため、もう少し複雑になります。このメソッドが、オーバーライドした onMeasure() メソッドから正常に呼び出されないと、測定時に例外が発生します。

onMeasure() の実装方法の概要を次に示します。

  1. オーバーライドした onMeasure() メソッドを呼び出す際は、幅と高さの測定値仕様(widthMeasureSpec パラメータと heightMeasureSpec パラメータ。どちらもサイズを示す整数のコード)を指定します。この仕様は、生成する幅と高さの測定値に対する制限の要件として扱われます。この仕様で要求される制限の種類の詳細については、View.onMeasure(int, int) のリファレンス ドキュメントをご覧ください(このリファレンス ドキュメントには、測定処理全般の詳しい説明もあります)。
  2. コンポーネントの onMeasure() メソッドは、コンポーネントのレンダリングに必要な幅と高さの測定値を計算します。このメソッドは通常、指定された仕様の範囲内で測定値を処理しようとしますが、仕様の超過も選択できます(その場合、親は、クリッピング、スクロール、例外のスローなどの処理方法を選択できます。また、別の測定仕様に基づいてもう一度測定を試みるよう onMeasure() に求めることもできます)。
  3. 幅と高さが計算されたら、計算された測定値を指定して setMeasuredDimension(int width, int height) メソッドを呼び出す必要があります。呼び出しが失敗すると、例外がスローされます。

フレームワークがビューで呼び出すその他の標準メソッドの概要を次に示します。

カテゴリ メソッド 説明
作成 コンストラクタ ビューがコードから作成されるときに呼び出される形式のコンストラクタと、ビューがレイアウト ファイルからインフレートされるときに呼び出される形式のコンストラクタがあります。後者の形式では、レイアウト ファイル内で定義されているすべての属性が解析されて適用されます。
onFinishInflate() このビューとそのすべての子が XML からインフレートされたときに呼び出されます。
レイアウト onMeasure(int, int) このビューとそのすべての子のサイズ要件を指定するために呼び出されます。
onLayout(boolean, int, int, int, int) このビューが自身のすべての子にサイズと位置を割り当てる必要があるときに呼び出されます。
onSizeChanged(int, int, int, int) このビューのサイズが変更されたときに呼び出されます。
描画 onDraw(Canvas) このビューが自身のコンテンツをレンダリングする必要があるときに呼び出されます。
イベント処理 onKeyDown(int, KeyEvent) 新しいキーイベントが発生したときに呼び出されます。
onKeyUp(int, KeyEvent) キーの UP イベントが発生したときに呼び出されます。
onTrackballEvent(MotionEvent) トラックボールのモーション イベントが発生したときに呼び出されます。
onTouchEvent(MotionEvent) タッチ スクリーンのモーション イベントが発生したときに呼び出されます。
フォーカス onFocusChanged(boolean, int, Rect) ビューがフォーカスを取得したときまたは喪失したときに呼び出されます。
onWindowFocusChanged(boolean) ビューを格納するウィンドウがフォーカスを取得したときまたは喪失したときに呼び出されます。
アタッチ onAttachedToWindow() ビューがウィンドウにアタッチされたときに呼び出されます。
onDetachedFromWindow() ビューがウィンドウからデタッチされたときに呼び出されます。
onWindowVisibilityChanged(int) ビューを格納するウィンドウの表示設定が変化したときに呼び出されます。

複合コントロール

フルカスタム コンポーネントを作成するのではなく、既存のコントロールを集めて再利用可能なコンポーネントにまとめたい場合は、複合コンポーネント(複合コントロール)の作成するだけで済みます。複合コンポーネントとは、簡単に説明すると、比較的基本的なコントロール(またはビュー)をいくつか集めてアイテムの論理グループを構成し、単体として扱えるようにする仕組みです。たとえば、コンボボックスは、単一行の EditText フィールドと、その横の PopupList 付きのボタンを組み合わせたものと考えることができます。ボタンを押してリストからアイテムを選択するとそのアイテムが EditText に入力されますが、ユーザーは好みに応じて EditText に直接入力することもできます。

実際に Android でこの仕組みを実現する場合は、SpinnerAutoCompleteTextView という 2 つの View を利用するほうが簡単ですが、コンボボックスは複合コンポーネントの概念を理解するのに役立ちます。

複合コンポーネントを作成するには:

  1. 通常はなんらかのタイプのレイアウトを出発点にして、レイアウトを拡張するクラスを作成します。たとえば、コンボボックスの場合、通常は水平方向の LinearLayout を使用します。他のレイアウトを内部にネストすることもできます。つまり、複合コンポーネントの構造はいくらでも複雑にすることができます。アクティビティと同様、宣言型の(XML ベースの)アプローチを使用して内部コンポーネントを作成することも、コードを使用してプログラムで内部コンポーネントをネストすることもできます。
  2. まず、新しいクラスのコンストラクタ内で、スーパークラスが必要とするパラメータをすべて受け取り、そのままスーパークラス コンストラクタに渡します。次に、新しいコンポーネントの内部で使用するその他のビューを設定します。通常は、ここで EditText フィールドと PopupList を作成します。なお、独自の属性とパラメータを XML に導入し、コンストラクタで抽出して使用することもできます。
  3. 内部ビューが生成するイベントのリスナーを作成することもできます。たとえば、リストアイテム クリック リスナー用のリスナー メソッドを使用して、リストアイテムが選択されたときに EditText の内容を更新できます。
  4. アクセサと修飾子を使用して独自のプロパティを作成することもできます。たとえば、コンポーネント内で最初に EditText 値を設定できるようにしておいて、必要なときにその内容をクエリできます。
  5. レイアウトを拡張する場合、onDraw() メソッドと onMeasure() メソッドをオーバーライドする必要はありません。レイアウトにはデフォルトの動作があり、通常はその動作で十分です。ただし、必要であればオーバーライドできます。
  6. 他の on... メソッドをオーバーライドすることもできます。たとえば、onKeyDown() をオーバーライドして、特定のキーが押されたときにコンボボックスのポップアップ リストからデフォルト値が選択されるようにすることができます。

要約すると、カスタム コントロールのベースとしてレイアウトを使用すると、以下のようなメリットが得られます。

  • アクティビティ画面と同様、宣言型の XML ファイルを使用してレイアウトを指定することも、コードを使用してプログラムでビューを作成し、レイアウト内にネストすることもできます。
  • onDraw() メソッドと onMeasure() メソッド(さらには他のほとんどの on... メソッド)には、通常、適切な動作が設定されているため、オーバーライドする必要はありません。
  • このようにして、複雑な複合ビューを簡単かつ自由に作成して、単体のコンポーネントであるかのように再利用できます。

既存のビュータイプを編集する

特定の状況では、もっと簡単にカスタムビューを作成する方法があります。必要とする機能と似た機能を備えたコンポーネントがすでに存在する場合は、そのコンポーネントを拡張して、変更したい動作をオーバーライドするだけで済みます。フルカスタム コンポーネントでできることはすべて実現できますが、それとともに、ビュー階層内の特化した用途を持つクラスから作業を始めることにより、必要な機能のほぼすべてを実現する多くの動作を自由に利用できます。

サンプルの NotePad アプリは、Android プラットフォームのさまざまな活用方法を紹介しています。その一例として、EditText ビューを拡張することで、罫線付きのメモ帳を作成しています。このサンプルは完全ではなく、罫線を付けるための API が変更される可能性がありますが、基本原則の理解には役立ちます。

まだ Android Studio に NotePad サンプルをインポートしていない場合は、インポートしてください(または、上記のリンクからソースを参照してください)。特に、NoteEditor.java ファイル内にある LinedEditText の定義をご覧ください。

このファイルの主なポイントは以下のとおりです。

  1. 定義

    クラスは次の行で定義されています。
    public static class LinedEditText extends EditText

    • LinedEditText は、NoteEditor アクティビティの内部クラスとして定義されていますが、パブリック クラスであるため、必要に応じて NoteEditor クラスの外部から NoteEditor.LinedEditText としてアクセスできます。
    • このクラスは static です。つまり、親クラスからのデータアクセスを許可する「合成メソッド」は生成しません。そのため、NoteEditor との強い関連性はなく、実際には独立したクラスとして動作します。 外部クラスから状態にアクセスする必要がない場合は、この単純な方法で内部クラスを作成できます。生成されるクラスは小さいため、他のクラスから簡単に利用できます。
    • このクラスは EditText(上記のサンプルでカスタマイズ対象として選択されているビュー)を拡張します。新しいクラスが完成したら、標準の EditText ビューの代わりとして利用できます。
  2. クラスの初期化

    通常どおり、先にスーパークラスを呼び出します。また、これはデフォルト コンストラクタではなく、パラメータ化コンストラクタです。EditText は、XML レイアウト ファイルからインフレートされるときに、このパラメータを使用して作成されます。そのため、このコンストラクタは、パラメータを受け取る処理と、受け取ったパラメータをスーパークラス コンストラクタに渡す処理の両方を行う必要があります。

  3. メソッドのオーバーライド

    上記のサンプルでオーバーライドされているメソッドは onDraw() だけですが、独自のカスタム コンポーネントを作成する場合は、必要に応じて他のメソッドもオーバーライドします。

    上記のサンプルでは、onDraw() メソッドをオーバーライドすることで、EditText ビューのキャンバス上に青色の線を描画できるようにしています(キャンバスは、オーバーライドされた onDraw() メソッドに渡されます)。メソッドが終了する前に、super.onDraw() メソッドが呼び出されています。このスーパークラス メソッドを実行するには、呼び出す必要があります。上記のサンプルでは、追加する線を描画した後、最後に呼び出しています。

  4. カスタム コンポーネントの使用

    カスタム コンポーネントを作成した後は、その使用方法を指定する必要があります。NotePad サンプルの場合、レイアウトの宣言から直接、カスタム コンポーネントを使用しています。そこで、res/layout フォルダにある note_editor.xml を見てみましょう。

    <view xmlns:android="http://schemas.android.com/apk/res/android"
        class="com.example.android.notepad.NoteEditor$LinedEditText"
        android:id="@+id/note"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/transparent"
        android:padding="5dp"
        android:scrollbars="vertical"
        android:fadingEdge="vertical"
        android:gravity="top"
        android:textSize="22sp"
        android:capitalize="sentences"
    />
    
    • カスタム コンポーネントは XML 内で汎用ビューとして作成されており、そのクラスはフルパッケージを使用して指定されています。また、定義した内部クラスが、NoteEditor$LinedEditText という表記を使用して参照されています。これは、Java プログラミング言語において、内部クラスを参照する際の標準的な方法です。

      カスタムビュー コンポーネントを内部クラスとして定義しない場合は、代わりに XML 要素名を使用して View コンポーネントを宣言し、class 属性を除外します。次に例を示します。

      <com.example.android.notepad.LinedEditText
        id="@+id/note"
        ... />
      

      この場合、LinedEditText クラスは独立したクラスファイルになります。このクラスが NoteEditor クラス内にネストされている場合、この方法は機能しません。

    • 定義内の他の属性とパラメータは、カスタム コンポーネント コンストラクタに渡され、そのまま EditText コンストラクタに渡されます。つまり、EditText ビューで使用するパラメータと同じパラメータになります。なお、独自のパラメータを追加することもできます。この点については別のセクションで説明します。

ポイントの説明は以上です。上記のサンプルはシンプルですが、ポイントは抑えています。必要に応じて、いくらでも複雑なカスタム コンポーネントを作成することができます。

高度なコンポーネントの場合、さらに多くの on... メソッドをオーバーライドし、そのヘルパー メソッドを導入することで、プロパティと動作を大幅にカスタマイズできます。想像力さえあれば、必要に応じて、自由にコンポーネントをカスタマイズできます。