6 月 3 日の「#Android11: The Beta Launch Show」にぜひご参加ください。

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

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

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

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

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

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

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

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

基本的なアプローチ

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

  1. 既存の View クラスまたはサブクラスを、独自のクラスを使用して拡張します。
  2. スーパークラスの一部のメソッドをオーバーライドします。オーバーライドの対象となるスーパークラス メソッドは、先頭に「on」が付いているものです(onDraw()onMeasure()onKeyDown() など)。これは、ライフサイクルなどの機能フックの際に、ActivityListActivityon... イベントをオーバーライドするのと同様です。
  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) キーアップ イベントが発生したときに呼び出されます。
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. 内部ビューが生成するイベントのリスナーを作成することもできます。たとえば、List Item Click Listener 用のリスナー メソッドを使用することで、リストアイテムが選択されたときに EditText の内容を更新することができます。
  4. アクセサや修飾子を使用して独自のプロパティを作成することもできます。たとえば、コンポーネント内で最初に EditText 値を設定しておいて、必要に応じてそのコンテンツを照会することができます。
  5. レイアウトを拡張する場合、onDraw() メソッドや onMeasure() メソッドをオーバーライドする必要はありません。レイアウトは通常、デフォルト動作で適切に機能します。必要に応じてオーバーライドすることもできます。
  6. 他の on... メソッドをオーバーライドすることもできます。たとえば、onKeyDown() をオーバーライドすることで、特定のキーが押されたときにコンボボックスのポップアップ リストから特定のデフォルト値が選択されるように設定できます。

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

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

既存の View タイプを編集する

場合によっては、さらに簡単な方法でカスタムビューを作成することができます。必要とする機能と似た機能を備えたコンポーネントがすでに存在している場合、そのコンポーネントを拡張し、変更したい動作をオーバーライドするだけで済みます。フルカスタム コンポーネントの場合と同様の設定が可能な一方、ビュー階層の中で用途が特化しているクラスをベースに作業を開始することで、労力をかけることなく、必要な動作をそのまま流用することができます。

たとえば、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 ビューのキャンバス上に青色の線を描画できるようにしています(キャンバスは、オーバーライドされた メソッドに渡されます)。メソッドが終了する前に、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... メソッドをオーバーライドし、そのヘルパー メソッドを導入することで、プロパティや動作を大幅にカスタマイズできます。想像力さえあれば、必要な機能に応じて、自由自在にコンポーネントをカスタマイズすることができます。