View on GitHub
mixi-inc/AndroidTraining
ActionBarとインタラクション制御
この章では、Honeycomb 以降に導入された Android 標準の UI コンポーネントである ActionBar と、インタラクション制御について解説します。

参考:UI デザインとしてのAction Bar | Android Developers
参考:プログラミングとしてのAction Bar | Android Developers

目次

ActionBar

Android の公式な UI デザインの中でも、特に重要な役割をもっているものが ActionBar です。
ユーザを適切にナビゲーションし、アプリの中での目的を達成するには、ActionBar の知識と実装は必須のものとなっています。

ActionBar の基本

ActionBar とは、アプリにある複数の画面で恒久的に表示される、主にナビゲーションと適切なアクションの提示のために利用される、重要な UI コンポーネントです。
ActionBar には、以下の 4 つのコンポーネントがあります。

ActionBar Basics

  1. App Icon
    • アプリのアイコンです。単なるアイコンとしての機能だけでなく、ホームに戻る、や、階層を 1 つ戻る、などの役割を持つこともできます。
  2. View Control
    • 異なる View に様々なデータを表示するような場合に、ユーザが素早く View を切り替えるためのコンポーネントです。右下に三角形のマークが付いているものは、ドロップダウンリストのメニューが表示されます。これ以外に、タブによる切り替えも用意されています。
  3. ActionItem Buttons(Action Buttons)
    • ここに配置されるボタン類は、アプリやその画面のなかで最も重要なアクションを促すために配置されます。画面幅などでこのエリア内に収まらないボタンは、自動的に、4 の Action Overflow に移されます。Action Buttons は、ActionBar を分割し、Action Buttons を画面下部に表示させることもできます。
  4. Action Overflow
    • 画面に収まらなかったり、意図的に Action Overflow に入るよう設定した Action Buttons がここにまとめられます。ここに収める Action Buttons は優先順位の低いものとして位置づけられます。

また、ActionBar を導入するにあたって、以下に上げる重要な要素があります。

ActionBar には、3 つのナビゲーションモードが用意されています。 1 つの Activity で選択できるモードは、このうち 1 つだけです。

Standard Navigation

デフォルトのナビゲーションモードです。

Standard Navigation

List Navigation

ドロップダウンのリストから、項目を 1 つ選択してコンテンツを切り替えるタイプのナビゲーションモードです。

List Navigation

List Navigation

// Honeycomb 以降向けのコード
public class SherlockListNavigationActivity extends Activity implements OnNavigationListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_sample);

        // List Navigation のリストに表示するもの
        List<String> list = new ArrayList<String>();
        list.add("Navi Menu 1");
        list.add("Navi Menu 2");
        list.add("Navi Menu 3");
        // ナビゲーションモードを List Navigation に設定
        getActionBar().setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
        // List Navigation の準備
        // ArrayAdapter は一覧表示のための、データと View をひもづけるためのクラス。詳細は別の章。
        getActionBar().setListNavigationCallbacks(
                new ArrayAdapter<String>(this,
                        android.R.layout.simple_list_item_1,
                        android.R.id.text1, list),
                this);
        // タイトルを表示しないようにする
        getActionBar().setDisplayOptions(0, ActionBar.DISPLAY_SHOW_TITLE);
    }

    // List Navigation の一覧から項目を選択したら呼び出されるコールバック
    @Override
    public boolean onNavigationItemSelected(int itemPosition, long itemId) {
        // ここで、どの項目が選択されたかを検知し、適切なイベント処理を行う
        // イベント処理が実行されたら、true を返すようにする

        return false;
    }
}

Tab Navigation

複数のタブから 1 つを選択してコンテンツを切り替えるタイプのナビゲーションモードです。

タブの位置は、画面の大きさによって決定されます。
横幅の狭い画面では、アクションバーの直下に表示されますが、広い画面では、アクションバーのタイトル部分の右横にタブが統合されます。
これは、端末の画面回転時にも判定が行われるため、縦画面と横画面で動的にアクションバーのレイアウトが切り替わることになります。

Tab Navigation

Tab Navigation

// Honeycomb 以降向けのコード
public class SherlockTabNavigationActivity extends Activity implements TabListener {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_sample);

        // タブナビゲーションモードに設定
        getActionBar().setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
        // タブを作成して追加。タブの選択・解除・再選択をハンドリングするコールバックの TabListener をセットしないと実行時例外でクラッシュする
        getActionBar().addTab(getSupportActionBar().newTab().setText("Tab1").setTabListener(this));
        getActionBar().addTab(getSupportActionBar().newTab().setText("Tab2").setTabListener(this));
        getActionBar().addTab(getSupportActionBar().newTab().setText("Tab3").setTabListener(this));
    }

    // タブナビゲーションの Tab が選択された時のコールバック
    @Override
    public void onTabSelected(Tab tab, FragmentTransaction ft) {
        
    }

    // タブナビゲーションの Tab が選択解除された時のコールバック
    @Override
    public void onTabUnselected(Tab tab, FragmentTransaction ft) {
        
    }

    // タブナビゲーションの Tab が再度選択された時のコールバック
    @Override
    public void onTabReselected(Tab tab, FragmentTransaction ft) {
        
    }
}

ActionItem

これまでの OptionsMenu(メニューキーを押して表示するメニュー)の代替として、アクションバーに含められるメニューです。
リソースは全て OptionsMenu のものを流用することが可能ですが、一部属性がアクションバー向けに追加されています。

android:showAsAction

アクションバーへの出し方を決定するための属性です。

意味
always 常に表示します。最も重要度の高いメニューとして扱います。
ifRoom 他の ActionBar のコンポーネントが占める領域を見て、空きがあれば表示します。
never 常に ActionBar 上には表示せず、その他のメニュー(3 つの点のアイコン)の中にまとめられます。最も重要度が低い項目に適用します。
withText 横幅の広い画面において、アイコンだけでなく、文言も一緒に表示するオプションです。

alwaysifRoomneverは一緒に指定することができません。必ずこの 3 つのどれかを選択します。

Action Item

ifRoom指定して画面に入りきらなかったものと、never指定したものは、下記のように Overflow にまとめられます。

Action Item

また、アイコンを設定せず、ラベルのみを設定した場合で、Action Item に並べられるときは、全て大文字になって表示されます。

Action Item

端末の画面サイズに応じて、ActionBar が自動で Action Item の表示をハンドリングしてくれます。

タブレット型のような大きなディスプレイでは、アイコンとラベルが同時に表示されますが、モバイル向けの端末では、横幅によってアイコンのみの場合と、ラベルとアイコンが同時に表示される場合とがあります。
横画面にしても、横幅が 480dp に満たない端末では、常にどちらかだけになります。

App Icon Navigation

標準で、アクションバーの左端には、アプリのアイコンが表示されています。

このアイコンを、ナビゲーションのための UI として機能させることができます。
Android の流儀としての使い方として、下記の 2 つが想定されています。

  • アプリのホーム画面(アプリの起動直後の画面)へ戻る、ホームボタンの役割
  • アプリのナビゲーションの階層構造を上に戻る、戻るボタンの役割

この 2 つの違いは、アイコンの表示にも影響します。

App Icon Navigation Difference

左が、ホームボタンとしての役割の時、右が、戻るボタンとしての役割の時の表示です。
戻るボタンとして機能させるときには、左向きの矢印が表示されます。

アイコンがタップされた時の制御については、後述するインタラクション制御で説明します。

Split ActionBar

Action Item を、画面上部の ActionBar から切り離し、画面下部に 1 列に表示するモードです。 AndroidManifest において、<activity>の属性として、android:uiOptionssplitActionBarWhenNarrowを設定することで実現出来ます。

Split ActionBar

ActionBarSherlock

ActionBar は Android 3.x で導入された新しい UI コンポーネントです。Android 4.x以降は、タブレットもモバイル端末も同じ OS で動かすことを標準に据えているので、Android 2.x でも ActionBar の導入が進んでいます。

一方、Google が公式にリリースしている Support Package には、Android 2.x でこのコンポーネントを使用出来るようにするものは含まれていません。よって、サードパーティ製のライブラリで、Android 2.x でも ActionBar を使えるようにします。

現在最も利用されているサードパーティ製ライブラリは、ActionBarSherlock です。

Android が標準で用意している API と異なる部分は以下のとおりです。

  • 画面コンポーネントを ActionBarSherlock のものに置き換える
    • SherlockActivity、SherlockFragmentActivity など
  • 標準 API と同じ名前のクラスで、ActionBarSherlock の名前空間にあるクラスに置き換える
    • Menu など
  • テーマを、ActionBarSherlock のものに置き換える
    • Theme.Sherlock、Theme.Sherlock.Light など
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="jp.mixi.sample.actionbar.sherlock"
    android:versionCode="1"
    android:versionName="1.0">

    <uses-sdk
        android:minSdkVersion="7"
        android:targetSdkVersion="17"/>

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/Theme.Sherlock">

        <!-- ActionBarSherlock の theme を使用するようにする -->
        <activity
            android:name=".MainActivity"
            android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <!-- Split ActionBar を実現するための属性指定 -->
        <activity
            android:name=".SherlockListNavigationActivity"
            android:label="@string/app_name"
            android:uiOptions="splitActionBarWhenNarrow">
        </activity>
        <activity
            android:name=".SherlockTabNavigationActivity"
            android:label="@string/app_name">
        </activity>
        <activity
            android:name=".SherlockStandardNavigationActivity"
            android:label="@string/app_name">
        </activity>
    </application>
</manifest>
package jp.mixi.sample.actionbar.sherlock;

import android.os.Bundle;
import android.widget.ArrayAdapter;

import com.actionbarsherlock.app.ActionBar;
import com.actionbarsherlock.app.ActionBar.OnNavigationListener;
import com.actionbarsherlock.app.SherlockActivity;
import com.actionbarsherlock.view.Menu;

import java.util.ArrayList;
import java.util.List;

public class SherlockListNavigationActivity extends SherlockActivity implements OnNavigationListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_sample);

        // List Navigation のリストに表示するもの
        List<String> list = new ArrayList<String>();
        list.add("Navi Menu 1");
        list.add("Navi Menu 2");
        list.add("Navi Menu 3");
        // ナビゲーションモードを List Navigation に設定
        getSupportActionBar().setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
        // List Navigation の準備
        getSupportActionBar().setListNavigationCallbacks(
                new ArrayAdapter<String>(getApplicationContext(),
                        android.R.layout.simple_list_item_1,
                        android.R.id.text1, list),
                this);
        // タイトルを表示しないようにする
        getSupportActionBar().setDisplayOptions(0, ActionBar.DISPLAY_SHOW_TITLE);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        // Menu が Android 標準 API のものとは違うので、区別するために getSupportMenuInflater() を呼ぶ
        getSupportMenuInflater().inflate(R.menu.main, menu);
        return true;
    }

    // List Navigation の一覧から項目を選択したら呼び出されるコールバック
    @Override
    public boolean onNavigationItemSelected(int itemPosition, long itemId) {
        return false;
    }
}

ActionBar の見た目を変える

Android Action Bar Style Generator などで、簡単に Drawable Resource や Style Resource を生成できるようになっています。

以下のように、Style Resource として各種 Drawable を利用してカスタマイズしていきます。

<resources>

    <!-- アクションバー の各種スタイルの設定をまとめた、テーマスタイル -->
    <style name="Theme.Example" parent="@style/Theme.Sherlock.Light">
        <!-- アクションバーの背景 Drawable -->
        <item name="actionBarItemBackground">@drawable/selectable_background_example</item>
        <!-- ポップアップで表示されるメニュー項目の Style -->
        <item name="popupMenuStyle">@style/PopupMenu.Example</item>
        <item name="dropDownListViewStyle">@style/DropDownListView.Example</item>
        <item name="actionBarTabStyle">@style/ActionBarTabStyle.Example</item>
        <item name="actionDropDownStyle">@style/DropDownNav.Example</item>
        <item name="actionBarStyle">@style/ActionBar.Solid.Example</item>
        <item name="actionModeBackground">@drawable/cab_background_top_example</item>
        <item name="actionModeSplitBackground">@drawable/cab_background_bottom_example</item>
        <item name="actionModeCloseButtonStyle">@style/ActionButton.CloseMode.Example</item>

        
    </style>

    <style name="ActionBar.Solid.Example" parent="@style/Widget.Sherlock.Light.ActionBar.Solid">
        <item name="background">@drawable/ab_solid_example</item>
        <item name="backgroundStacked">@drawable/ab_stacked_solid_example</item>
        <item name="backgroundSplit">@drawable/ab_bottom_solid_example</item>
        <item name="progressBarStyle">@style/ProgressBar.Example</item>
    </style>

    <style name="ActionBar.Transparent.Example" parent="@style/Widget.Sherlock.Light.ActionBar">
        <item name="background">@drawable/ab_transparent_example</item>
        <item name="progressBarStyle">@style/ProgressBar.Example</item>
    </style>
	
    <style name="PopupMenu.Example" parent="@style/Widget.Sherlock.Light.ListPopupWindow">	
        <item name="android:popupBackground">@drawable/menu_dropdown_panel_example</item>	
    </style>
	
    <style name="DropDownListView.Example" parent="@style/Widget.Sherlock.Light.ListView.DropDown">
        <item name="android:listSelector">@drawable/selectable_background_example</item>
    </style>

    <style name="ActionBarTabStyle.Example" parent="@style/Widget.Sherlock.Light.ActionBar.TabView">
        <item name="android:background">@drawable/tab_indicator_ab_example</item>
    </style>
	
    <style name="DropDownNav.Example" parent="@style/Widget.Sherlock.Light.Spinner.DropDown.ActionBar">
        <item name="android:background">@drawable/spinner_background_ab_example</item>
        <item name="android:popupBackground">@drawable/menu_dropdown_panel_example</item>
        <item name="android:dropDownSelector">@drawable/selectable_background_example</item>
    </style>
    
    <style name="ProgressBar.Example" parent="@style/Widget.Sherlock.Light.ProgressBar.Horizontal">
        <item name="android:progressDrawable">@drawable/progress_horizontal_example</item>
    </style>
    
    <style name="ActionButton.CloseMode.Example" parent="@style/Widget.Sherlock.Light.ActionButton.CloseMode">
        <item name="android:background">@drawable/btn_cab_done_example</item>
    </style>
    
    <!-- this style is only referenced in a Light.DarkActionBar based theme -->
    <style name="Theme.Example.Widget" parent="@style/Theme.Sherlock">
        <item name="popupMenuStyle">@style/PopupMenu.Example</item>
        <item name="dropDownListViewStyle">@style/DropDownListView.Example</item>
    </style>

</resources>

インタラクション制御

Activity や Fragment のコールバックメソッド

Activity と Fragment には、メニューの構成と表示について、下記のような 2 種類のメニューを扱うコールバックメソッドが用意されています。

OptionsMenu

メニューキーで表示されるメニュー、また ActionBar に表示される ActionItem の構成を扱います。

下記のコールバックメソッドが用意されています。

メソッド名 意味
onPrepareOptionsMenu OptionsMenu を表示する前に、メニュー要素の状態を管理するために呼び出される
onCreateOptionsMenu OptionsMenu の構成を初期化するために、Activity や Fragment のライフサイクルの中で 1 度だけ呼ばれる
onOptionsItemSelected OptionsMenu の選択状況を見て、適切なアクションを起こすために呼び出される
public class MainActivity extends Activity {
    @Override
    public boolean onPrepareOptionsMenu(Menu menu) {
        // ここで、状態に応じてメニューの有効・無効を切り替えたりなどの処理をする
        return super.onPrepareOptionsMenu(menu);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // ここで、この Activity で利用するメニューのリソースを読み込む
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // 選択されたメニューに対応するイベント処理をここで実行する
        return super.onOptionsItemSelected(item);
    }
}

ContextMenu

View の長押しで表示されるメニューの構成を扱います。

下記のコールバックメソッドが用意されています。

メソッド名 意味
onCreateContextMenu ContextMenu の構成を初期化する
onContextItemSelected ContextMenu の選択状況を見て、適切なアクションを起こすために呼び出される
public class MainActivity extends Activity {
    @Override
    protected void onStart() {
        super.onStart();

        // View に長押しメニュー用のコールバックを設定する
        // 実際には、View に OnCreateContextMenuListener を設定するだけ
        // 登録処理を Activity が肩代わりしている
        View helloWorld = findViewById(R.id.HelloWorld);
        registerForContextMenu(helloWorld);
    }

    @Override
    protected void onStop() {
        // View に設定した長押しメニュー用のコールバックを解除する
        View helloWorld = findViewById(R.id.HelloWorld);
        unregisterForContextMenu(helloWorld);

        super.onStop();
    }

    @Override
    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
        // ここで、長押しメニューで利用するメニューリソースを読み込む
        super.onCreateContextMenu(menu, v, menuInfo);
    }

    @Override
    public boolean onContextItemSelected(MenuItem item) {
        // ここで、選択されたメニューリソースに対応するイベント処理を実行する
        return super.onContextItemSelected(item);
    }
}

View の状態とコールバックメソッド

View には、以下のような「状態(State)」が定義されています。
View の種類によって、取り得る状態は様々です。

状態名 意味
normal 何もしていない
enabled View がユーザ操作を受け付ける状態
focused View がユーザ操作によってフォーカスを持っている状態
selected View がユーザ操作によって選択されている状態
checked View がユーザ操作によってチェックされている状態
pressed View がユーザ操作によって押されている(クリックされている・タップされている)状態

これらそれぞれの状態について、Observer パターンでイベントを監視するための仕組みが用意されています。
監視するためのオブジェクトの登録方法によっては、Activity のライフサイクルに合わせて監視オブジェクトの管理を剃る必要のあるものがあることに注意してください。

OnClickListener

View をタップした時に呼び出されるイベントを拾うための Observer です。

view.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
    }
});

OnLongClickListener

View を長押しした時に呼び出されるイベントを拾うための Observer です。
長押しした時のイベント処理が適切に実行されたら、trueを返します。

view.setOnLongClickListener(new View.OnLongClickListener() {
    @Override
    public boolean onLongClick(View v) {
        return false;
    }
});

OnFocusChangedListener

View のフォーカスが移動した時のイベントを拾うための Observer です。

view.setOnFocusChangeListener(new View.OnFocusChangeListener() {
    @Override
    public void onFocusChange(View v, boolean hasFocus) {
    }
});

OnCheckedChangeListener

CheckBox などで、チェック状態が変化した時のイベントを拾うための Observer です。

CompoundButton checkBox = (CompoundButton) findViewById(R.id.HelloCheck);
checkBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
    @Override
    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
    }
});

TextWatcher

EditText などで、入力テキストが変更された時のイベントを拾うための Observer です。 こちらは、Activity や Fragment のライフサイクルに合わせて、適宜 View への登録と解除を行う必要があります。

public class MainActivity extends Activity {
    private TextWatcher mTextWatcher = new TextWatcher() {
        @Override
        public void onTextChanged(CharSequence s, int start, int before, int count) {}

        @Override
        public void beforeTextChanged(CharSequence s, int start, int count, int after) {}

        @Override
        public void afterTextChanged(Editable s) {}
    };

    @Override
    protected void onStart() {
        super.onStart();

        // ライフサイクルに合わせて、Observer オブジェクトを登録する
        TextView helloEdit = (TextView) findViewById(R.id.HelloEdit);
        helloEdit.addTextChangedListener(mTextWatcher);
    }

    @Override
    protected void onStop() {
        // ライフサイクルに合わせて、Observer オブジェクトを解除する
        TextView helloEdit = (TextView) findViewById(R.id.HelloEdit);
        helloEdit.removeTextChangedListener(mTextWatcher);

        super.onStop();
    }
}

実習・課題

ActionBar

  1. (実習) 実習用プロジェクトが、libraries/ActionBarSherlock を参照するように設定してください
  2. (実習) 1. で設定したプロジェクトで、ActionBar の TabNavigation を実現し、3 つのタブを持つ画面を作成してください
  3. (実習) TabNavigation で、タブを選択された時に、どのタブが選択されたかをToastで表示するようにしてください(プロジェクトは実習1, 2 のものを利用する)
  4. (課題) 新しい Activity を作成し、その Activity の中で App Icon Navigation を有効にした上で、階層を戻るボタンとしての役割を実装してください
  5. (課題) 3 つのタブと、2 つの ActionItem を持つ ActionBar を MainActivity に実装してください。ActionItem のうち、片方は新しい Activity を起動するように、もう片方は MainActivity を終了するようにイベントをハンドリングしてください。

インタラクション制御

  1. (実習) OptionsMenu に、以下の 3 つの項目を表示し、それぞれ選択時に、どのメニューが選択されたかをToastで表示するようにしてください
    • Settings と表示するもの
    • Login と表示するもの
    • Refresh と表示するもの
  2. (実習) ActionBarSherlock を参照する実習用プロジェクトで OptionsMenu を扱った場合の、OptionsMenu の表示のされ方の違いを、エミュレータを用いて、画面の大きさを元に考察してレポートに記述してください(android:showAsActionwithTextをつけると違いが出てきます。)
  3. (課題) OptionsMenu に、以下の 2 つの項目を表示し、そのうち一方の項目の有効/無効の状態を交互に切り替える処理を実装してください。
    • Settings と表示するもの
    • Refresh と表示するもの
  4. (課題) TextWatcher を利用して、EditText に入力された文字数をカウントする TextView を作成してください。