カスタムビュー コンポーネントを作成する

Compose をお試しください
Jetpack Compose は、Android で推奨される UI ツールキットです。Compose でレイアウトを扱う方法について説明します。

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() など)。これは、ライフサイクルなどの機能フックの際に、Activity または ListActivityon イベントをオーバーライドするのと同様です。
  3. 新しい拡張クラスを使用します。完成した新しい拡張クラスは、ベースとなったビューの代わりとして使用できます。

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

フルカスタム コンポーネントを使用すれば、あらゆる外観のグラフィカル コンポーネントを自由に作成できます。たとえば、アナログ目盛りのようなグラフィカルなボリューム メーターや、カラオケで曲に合わせて歌を歌えるようにボールがバウンドしながら歌詞の間を移動するテキストビューなどを作成できます。組込みコンポーネントを組み合わせるだけでは実現できないものが必要になる場合があります。

幸いなことに、コンポーネントは想像力、画面サイズ、利用可能な処理能力の範囲内で、望みどおりの外観と動作を実現できます。ただし、アプリケーションはデスクトップ ワークステーションよりも大幅に処理能力の低いデバイスで実行される可能性があることを念頭に置いてください。

フルカスタム コンポーネントを作成する際は、次の点を考慮してください。

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

onDraw() と onMeasure() を拡張する

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

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

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

  • オーバーライドした onMeasure() メソッドを呼び出す際は、幅と高さの仕様を指定します。この仕様は、生成する幅と高さの測定値に対する制限の要件として扱われます。widthMeasureSpec パラメータと heightMeasureSpec パラメータはどちらもディメンションを表す整数コードです。この仕様で要求される制限の種類の詳細については、View.onMeasure(int, int) のリファレンス ドキュメントをご覧ください。このリファレンス ドキュメントには、測定処理全般の説明もあります。
  • コンポーネントの onMeasure() メソッドは、コンポーネントのレンダリングに必要な幅と高さの測定値を計算します。渡された仕様内に留まるように試みる必要がありますが、仕様を超えることもあります。この場合、親はクリッピング、スクロール、例外のスロー、onMeasure() に再試行を求める(おそらく異なる測定仕様で)など、何を行うかを選択できます。
  • 幅と高さが計算されたら、計算された測定値を指定して 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 フィールドと、その横のポップアップ リスト付きのボタンを組み合わせたものと考えることができます。ボタンをタップしてリストからアイテムを選択すると、選択したアイテムが EditText フィールドに入力されますが、ユーザーは必要に応じて EditText に直接入力することもできます。

Android では、SpinnerAutoCompleteTextView という 2 つの View を利用する方が簡単です。いずれにしても、このコンボボックスのコンセプトは良い例になります。

複合コンポーネントを作成する手順は次のとおりです。

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

カスタム コントロールのベースとして Layout を使用するメリットは次のとおりです。

  • アクティビティ画面と同様、宣言型の 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 としてアクセスできます。

    また、LinedEditTextstatic です。つまり、親クラスからのデータアクセスを許可する「合成メソッド」は生成しません。つまり、NoteEditor との強い関連性はなく、独立したクラスとして動作します。外部クラスから状態にアクセスする必要がない場合は、この単純な方法で内部クラスを作成できます。生成されるクラスは小さいため、他のクラスから簡単に利用できます。

    LinedEditTextEditText を拡張します。この場合、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 要素名を使用してビュー コンポーネントを宣言し、class 属性を除外できます。次に例を示します。

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

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

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

カスタム コンポーネントの作成は、必要に応じて複雑にすることができます。

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