View on GitHub
mixi-inc/AndroidTraining
続・アプリのレイアウト作成
この章では、レイアウトの作成に関してより発展的な内容を解説します。

参考:Re-using Layouts with | Android Developers
参考:Dialogs | Android Developers
参考:Settings | Android Developers

目次

レイアウトのインクルード

<include>

<include>タグは別のレイアウトxmlを挿入する機能を提供します。
<include>を使用することでレイアウトを再利用でき、アプリのコンポーネントが共通化されUIが統一できます。また、修正する際のコストが下がります。レイアウトが複雑でコードが肥大化する場合は、コードの分割に使用することで可読性が向上し、レイアウトの管理や各コンポーネントの配置替えが容易になります。

button_pannel.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal" >

    <Button
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:text="@string/button_1" />

    <Button
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:text="@string/button_2" />

    <Button
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:text="@string/button_3" />

</LinearLayout>

include_main.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity" >

    <TextView
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:text="@string/include_sample" />

    <!-- 別のレイアウトファイルを挿入 -->
    <include layout="@layout/button_pannel" />

</LinearLayout>

include_sample

<include>にはandroid:idを設定することができるので、同じレイアウトを include しても個別に管理することができます。

<merge>

<merge>タグはレイアウトを合わせる機能を提供します。

通常レイアウトファイルは Viewgroup をルートViewとして配置していますが、<merge>タグに置き換えることができます。
置き換えたレイアウトを include すると<merge>タグは無視され、内部のレイアウトが include 先のレイアウトに直接とりこまれることになります。ViewGroup を取り除くことができるため、View の階層が1つ浅くなり描画コストが減ることになります。   

merge適用前のレイアウト:button_no_apply_merge.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" >

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/button_1" />

</FrameLayout>

merge適用後のレイアウト:button_apply_merge.xml

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android" >

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/button_1" />

</merge>

merge_main.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity" >

    <TextView
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:text="@string/merge_sample" />

    <!-- マージされたレイアウトファイルを挿入 -->
    <include layout="@layout/button_apply_merge" />

</LinearLayout>

merge_sample

上記レイアウトをそれぞれ実行し、Hierarchy View でレイアウト階層を確認すると以下のようになり、FrameLayoutが取り除かれているのがわかります。
merge_no_apply_sample merge前
merge_apply_sample merge後

Dialog

Dialog はユーザーに意思決定や情報の入力を促すための小さいウィンドウです。
画面いっぱいに表示するものではなく、何らかの処理を行う前にユーザーへアクションを要求するのに使用します。

この他、進捗状況を知らせる為にも Dialog が使われます。

Dialog の表示には DialogFragment を使用することが推奨されています。 DialogFragment は Android 3.0 から使用出来る機能ですが、Support Library で提供されているため Android 2.x系でも使用することができます。

Dialog のデザイン

ユーザへ意思決定を求める Dialog を、AlertDialog と言います。
AlertDialog の構成は以下のようになっています。

dialogs_regions

  1. タイトルです。必須ではなくコンテンツ領域が詳細なメッセージなどで埋められている場合などに使用します。
  2. コンテンツ領域です。メッセージを表示します。また、独自のレイアウトに変更することもできます。
  3. アクションボタンです。ボタンの数は3つ以下とします。

一方、進捗状況を知らせる Dialog を、ProgressDialog と言います。

それぞれ、Dialog を表示すると、表示領域外がグレーアウトします。
このグレーアウトした領域をタップすると、キャンセル扱いとすることができるようになっています。

FragmentDialog の

以下の手順が必要となります。

  1. DialogFragment を作成
  2. コンテンツ領域に独自レイアウトを使用するなら作成
  3. Dialog の表示処理を行う

通常の Dialog を表示する

独自のDialogFragment

    /**
     * Dialogを使用して、コンテンツ領域に独自レイアウトは表示するサンプルです。
     */
    public static class MyDialogFragment extends DialogFragment {

        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
            // 独自のレイアウトをコンテンツ領域表示する場合、ここでViewをinfrateして返却する
            return inflater.inflate(R.layout.dialog_content, container, false);
        }

        @Override
        public Dialog onCreateDialog(Bundle savedInstanceState) {
            // Dialogを生成
            Dialog dialog = super.onCreateDialog(savedInstanceState);
            // タイトルを設定
            dialog.setTitle(R.string.my_dialog_fragment);
            return dialog;
        }

    }

コンテンツ領域に表示する独自レイアウト : dialog_content.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".MainActivity" >

    <ImageView
        android:id="@+id/imageView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_alignParentTop="true"
        android:src="@drawable/ic_launcher" />

    <TextView
        android:id="@+id/textView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignTop="@+id/imageView1"
        android:layout_marginLeft="17dp"
        android:layout_marginTop="15dp"
        android:layout_toRightOf="@+id/imageView1"
        android:text="@string/custom_content" />

</RelativeLayout>

ダイアログの表示
MainActivity.java

        DialogFragment myDialogFragment = new MyDialogFragment();
        // 引数にFramentManagerとtagを設定します
        myDialogFragment.show(getSupportFragmentManager(), "my_dialog_fragment");

dialog_sample

AlertDialog を表示する

   /**
     * AlertDialogを使用するサンプルです。コンテンツ領域に独自レイアウトは表示しません。
     */
    public static class MyAlertDialogFragment extends DialogFragment {

        @Override
        public Dialog onCreateDialog(Bundle savedInstanceState) {

            // AlertDialogはBuilderパターンで生成
            AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());

            builder.setMessage(R.string.alert_dialog_message)
                    // OKボタン
                    .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
                        public void onClick(DialogInterface dialog, int id) {
                            Toast.makeText(getActivity(), "Positive", Toast.LENGTH_SHORT).show();
                        }
                    })
                    // Cancelボタン
                    .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
                        public void onClick(DialogInterface dialog, int id) {
                            Toast.makeText(getActivity(), "Negative", Toast.LENGTH_SHORT).show();
                        }
                    });
            // Dialogを作成して返却
            return builder.create();
        }
    }

alertdialog_sample

AlertDialog を表示する際の注意点

   /**
     * AlertDialogを使用しつつ、コンテンツ領域に独自レイアウトを表示するサンプルです。
     */
    public static class ErrorMyAlertDialogFragment extends DialogFragment {

        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
            // Dialogと同じように表示したいコンテンツをinfrateして返却するとクラッシュします。
            return inflater.inflate(R.layout.dialog_content, container, false);
        }

        @Override
        public Dialog onCreateDialog(Bundle savedInstanceState) {
            AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
            return builder.create();
        }

    }

onCreateDialog() で Dialog オブジェクトを返却する場合、onCreateView() でコンテンツ View を返却しようとするとクラッシュしてしまいます。
回避するには onCreateView() で null を返却するか、onCreateView() をオーバライドしないようにします。
代わりに、コンテンツ View は builder の setView メソッドで設定します。

設定画面

設定画面を作成する場合、他のアプリケーションとUI/UXの整合性がとれたインターフェースを提供する必要があります。
Android では設定画面に共通なUI/UXを提供できるようにするために Preference APIs が用意されています。Preference APIs を使用することで他のアプリケーションとの設定画面のUI/UXを統一することができます。
また、設定画面で変更されたデータは自動的に SharedPreferences に保存されます。
そのため、設定情報の保存処理を自身で実装する必要がなくなります。

設定画面を作成するには以下の手順が必要となります。

  1. 設定画面に表示する情報を xml に定義
  2. 表示する PreferenceActivity または PreferenceFragment を作成

設定情報を xml に定義

xml の保存先

  • xml ファイルはres/xmlディレクトリに格納する必要があります。
  • ファイル名は任意で決めることができますが、一般的にpreferences.xmlとすると良いです。
  • マルチペインのレイアウトを作成した場合は、Fragment毎に個別のxmlファイルが必要です。

xmlの定義

xml ファイルのルートタグは<PreferenceScreen> とします。 <PreferenceScreen> に追加された子要素(CheckBoxPreferenceListPreferenceEditTextPreference)は、設定画面の内の1つの項目として表示されます。 また、<PreferenceScreen>内に<PreferenceScreen>を配置することも可能です。

res/xml/preferences.xml

<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" >

    <CheckBoxPreference
        android:defaultValue="true"
        android:key="sample_checkbox_key"
        android:summary="サンプルです"
        android:title="チェックボックスのサンプル" />

    <ListPreference
        android:defaultValue="1"
        android:dependency="sample_checkbox_key"
        android:dialogTitle="リスト選択ダイアログ"
        android:entries="@array/item_entries"
        android:entryValues="@array/item_entries_value"
        android:key="sample_list_key"
        android:title="リストのサンプル" />

    <EditTextPreference
        android:dialogTitle="エディットテキストダイアログ"
        android:key="edit_texit_key"
        android:title="エディットテキストのサンプル" />

</PreferenceScreen>

res/values/array.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <string-array name="item_entries">
        <item>アイテム1</item>
        <item>アイテム2</item>
        <item>アイテム3</item>
    </string-array>

    <string-array name="item_entries_value">
        <item>one</item>
        <item>two</item>
        <item>three</item>
    </string-array>

</resources>

preference_sample

名前 説明
android:key SharedPreferences に保存する際に使用するキーとなります。要素が PreferenceCategory か PreferenceScreen かintentの呼び出し設定がされている場合は必須ではありません。
android:title 設定画面に表示するタイトルです
android:defaultValue システムに SharedPreferences へ初期値を設定するように指定します
android:entries ListPreferenceで表示するデータリスト
android:entryValues ListPreferenceで選択したときに保存される値。android:entries と key-value な関係となります
android:dependency 他のpreferenceのandroid:keyを設定します。設定したpreferenceが未設定またはOFFの場合はdisableとなります。
グループの作成

設定項目がいくつもあった場合、ユーザーは設定したい項目がどこにあるのか見つけるのに苦労してしまいます。
PreferenceCategoryを使用することにより設定項目を関連するものどうしで分けることができ、表示をわかりやすくできます。
res/xml/preferences_category.xml

<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" >

    <!-- グループ分けされます -->
    <PreferenceCategory
        android:key="pref_key_storage_settings"
        android:title="カテゴリ1" >
        <CheckBoxPreference
            android:key="category1_checkbox1_key"
            android:title="カテゴリ1チェックボックス1" />
        <CheckBoxPreference
            android:key="category1_checkbox2_key"
            android:title="カテゴリ1チェックボックス2" />
    </PreferenceCategory>

    <!-- グループ分けされます -->
    <PreferenceCategory
        android:key="pref_key_storage_settings"
        android:title="カテゴリ2" >
        <CheckBoxPreference
            android:key="category2_checkbox1_key"
            android:title="カテゴリ2チェックボックス1" />
    </PreferenceCategory>

</PreferenceScreen>

preference_category

サブスクリーンの作成

グループとは別にPreferenceScreen内にPreferenceScreenを配置することで、サブスクリーンを作成して階層的に表示することができます。
res/xml/preferences_subscreen.xml

<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" >

    <!-- 別階層になります -->
    <PreferenceScreen
        android:key="subscreen_key"
        android:title="サブスクリーン" >
        <CheckBoxPreference
            android:key="subscreen_checkbox1_key"
            android:title="サブスクリーンチェックボックス1" />
        <CheckBoxPreference
            android:key="subscreen_checkbox2_key"
            android:title="サブスクリーンチェックボックス2" />
    </PreferenceScreen>

    <CheckBoxPreference
        android:key="screen_checkbox1_key"
        android:title="チェックボックス1" />

</PreferenceScreen>

preference_subscreen_1  preference_subscreen_2

Intentを使用する

設定画面の変わりに、別のActivityやWebページを表示することができます。
res/xml/preferences_intent.xml

<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" >

    <!-- Web表示 -->
    <Preference android:title="Intent Web" >
        <intent
            android:action="android.intent.action.VIEW"
            android:data="http://mixi.jp/" />
    </Preference>

    <!-- Activityを起動 -->
    <Preference android:title="Intent Activity" >
        <intent
            android:targetClass="jp.mixi.sample.preference.SubActivity"
            android:targetPackage="jp.mixi.sample.preference" />
    </Preference>

</PreferenceScreen>

preference_intent

PreferenceActivity と PreferenceFragment

設定画面を表示するためには PreferenceActivity を継承した Acitivity を作成し、使用します。
PreferenceActivity は設定が更新されると自動的に SharedPreferences に値を保存します。

アプリケーションのターゲットをAndroid3.0以降のものみとする場合はPreferenceFragmentを使用することが推奨されています。

PreferenceActivity の使い方

onCreate() 内で addPreferencesFromResource() の引数として表示する設定ファイルを渡します。
SettingsActivity.java

public class SettingsActivity extends PreferenceActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        addPreferencesFromResource(R.xml.preferences);
    }

}
PreferenceFragment の使い方

Fragment の onCreate() 内で addPreferencesFromResource() の引数として表示する設定ファイルを渡し、 Activity で Fragment を追加して表示します。
SettingsActivity2.java

public class SettingsActivity2 extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // ActivityにFragmentを追加
        getFragmentManager().beginTransaction()
                .replace(android.R.id.content, new SettingsFragment())
                .commit();
    }

    public static class SettingsFragment extends PreferenceFragment {
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);

            addPreferencesFromResource(R.xml.preferences);
        }
    }

}
設定変更時のコールバック

設定情報が変更されたときのコールバックメソッドが提供されています。
コールバックメソッドを使用することで、変更時に設定情報のサマリーを一緒に変更するといったことが可能となります。
SettingsActivity3.java

/**
 * 設定変更時のコールバックサンプルです。
 */
public class SettingsActivity3 extends PreferenceActivity implements OnSharedPreferenceChangeListener {

    // EditTextPreference で使用しているkey
    private static final String EDIT_TEXT_KEY = "edit_texit_key";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        addPreferencesFromResource(R.xml.preferences);
    }

    @Override
    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
        // 処理対象のPreferenceかをkeyで判定
        if (key.equals(EDIT_TEXT_KEY)) {
            // keyからPreferenceのインスタンスを取得
            EditTextPreference editPref = (EditTextPreference) findPreference(EDIT_TEXT_KEY);
            // Preferenceのサマリーを変更
            editPref.setSummary(editPref.getText());
        }
    }

    @Override
    protected void onResume() {
        super.onResume();
        // リスナーを登録
        getPreferenceScreen().getSharedPreferences()
                .registerOnSharedPreferenceChangeListener(this);
    }

    @Override
    protected void onPause() {
        super.onPause();
        // リスナーを登録解除
        getPreferenceScreen().getSharedPreferences()
                .unregisterOnSharedPreferenceChangeListener(this);
    }

}

実習・課題

プロジェクトの開き方は課題プロジェクトの開き方を参照してください。

実習

この実習では2-3-AdvancedLayoutPracticeを使用します。

include

  1. IncludeActivity を使用し、include を使用したレイアウトを作成し、merge の有り無しで View の階層がどのようになるのかを Hierarchy View で確認してレポートしてください。

Dialog

  1. Dialog1Activityを使用し、下記の画像のような Dialog を表示してください。
  2. Dialog2ActivityとListItemSelectionDialogFragment のTODO コメントの指示通りのプログラムを記述してください。

設定画面

  1. PreferencePracticeActivityを下記の画像のような設定画面になるように実装してください。

課題

この課題では2-3-AdvancedLayoutAssignmentを使用します。

Dialog

  1. DialogActivityを下記の画像のようになるよう実装してください。