page.title=順応性のある UI フローの実装 parent.title=複数画面のデザイン parent.link=index.html trainingnavtop=true previous.title=さまざまな画面密度のサポート previous.link=screendensities.html @jd:body

このレッスンでの学習内容

  1. 現在のレイアウトを判別する
  2. 現在のレイアウトに合わせて応答する
  3. 他のアクティビティのフラグメントを再利用する
  4. 画面設定の変更を処理する

関連ドキュメント

試してみる

アプリが現在表示しているレイアウトによって、UI フローが異なる可能性があります。たとえば、アプリがデュアルペイン モードであれば、左ペインのアイテムをクリックすると、単に右ペインにコンテンツが表示されるだけですが、シングルペイン モードであれば、コンテンツは(別のアクティビティ内の)コンテンツ専用のペインに表示される必要があります。

現在のレイアウトを判別する

レイアウトによって実装が多少異なるので、まず、ユーザーが現在どのようなレイアウトを表示しているかを判別する必要があります。たとえば、ユーザーが表示しているレイアウトが「シングルペイン」モードなのか、「デュアルペイン」モードなのかを確認する必要があります。それは、以下のようなコードで、ある特定のビューが存在し、かつ可視になっているかを照会することで可能です:

public class NewsReaderActivity extends FragmentActivity {
    boolean mIsDualPane;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main_layout);

        View articleView = findViewById(R.id.article);
        mIsDualPane = articleView != null && 
                        articleView.getVisibility() == View.VISIBLE;
    }
}

このコードにおいて「article」ペインが使用可能かどうかを照会している点に注目してください。特定のレイアウトの照会をハードコーディングするよりもはるかに柔軟性があります。

その他にも、さまざまなコンポーネントでも対応できる方法として、コンポーネントを操作する前に使用可能かどうかを確認する方法もあります。たとえば、News Reader サンプル アプリでは、メニューを開くボタンがありますが、このボタンは Android 3.0 よりも古いバージョンで動作しているときにしか表示されません(この機能は、API レベル 11 以上の {@link android.app.ActionBar} で提供されるため)。そこで、以下のようなコードを追加して、このボタンのイベント リスナーを追加します:

Button catButton = (Button) findViewById(R.id.categorybutton);
OnClickListener listener = /* create your listener here */;
if (catButton != null) {
    catButton.setOnClickListener(listener);
}

現在のレイアウトに合わせて応答する

現在のレイアウトによって、一部のアクションの結果が異なる可能性があります。たとえば、News Reader サンプルでは、見出しリストで見出しをクリックしたとき、デュアルペイン モードの UI の場合は右ペインに記事が表示されますが、シングルペインの UI の場合は別のアクティビティが起動します:

@Override
public void onHeadlineSelected(int index) {
    mArtIndex = index;
    if (mIsDualPane) {
        /* display article on the right pane */
        mArticleFragment.displayArticle(mCurrentCat.getArticle(index));
    } else {
        /* start a separate activity */
        Intent intent = new Intent(this, ArticleActivity.class);
        intent.putExtra("catIndex", mCatIndex);
        intent.putExtra("artIndex", index);
        startActivity(intent);
    }
}

同様に、アプリがデュアルペイン モードの場合は、ナビ用タブでアクション バーを設定し、一方、シングルペイン モードの場合は、スピナー ウィジェットでナビを設定することになります。したがって、コードでは以下のようにどちらのケースが適切かを調べることも必要です:

final String CATEGORIES[] = { "トップ ニュース 政治", "政治", "経済", "Technology" };

public void onCreate(Bundle savedInstanceState) {
    ....
    if (mIsDualPane) {
        /* use tabs for navigation */
        actionBar.setNavigationMode(android.app.ActionBar.NAVIGATION_MODE_TABS);
        int i;
        for (i = 0; i < CATEGORIES.length; i++) {
            actionBar.addTab(actionBar.newTab().setText(
                CATEGORIES[i]).setTabListener(handler));
        }
        actionBar.setSelectedNavigationItem(selTab);
    }
    else {
        /* use list navigation (spinner) */
        actionBar.setNavigationMode(android.app.ActionBar.NAVIGATION_MODE_LIST);
        SpinnerAdapter adap = new ArrayAdapter(this, 
                R.layout.headline_item, CATEGORIES);
        actionBar.setListNavigationCallbacks(adap, handler);
    }
}

他のアクティビティのフラグメントを再利用する

複数の画面に対応するように設計する場合、あるパターンが繰り返されますが、そうしたパターンは、ある画面設定ではペインとして、別の画面設定では別のアクティビティとして実装されるインターフェースの一部に存在します。たとえば、News Reader サンプルでは、ラージ画面の場合はニュース記事のテキストが右ペインに表示されますが、それよりも小さい画面の場合は別のアクティビティになります。

このような場合、通常、複数のアクティビティで同じ {@link android.app.Fragment} サブクラスを再利用することでコードの重複を回避できます。たとえば、ArticleFragment は以下のようにデュアルペイン レイアウトで使用されます:

{@sample development/samples/training/multiscreen/newsreader/res/layout/twopanes.xml all}

また、より小さな画面向けのアクティビティ レイアウト内では(レイアウトを使用せずに)再利用されます(ArticleActivity):

ArticleFragment frag = new ArticleFragment();
getSupportFragmentManager().beginTransaction().add(android.R.id.content, frag).commit();

当然、これは XML レイアウトでフラグメントを宣言するのと同じ効果がありますが、この場合は、XML レイアウトは必要ありません。このアクティビティのコンポーネントは記事フラグメントしかないからです。

フラグメントを設計する際に注意すべき非常に重要なポイントの 1 つとして、特定のアクティビティに対して強い結合を作成しないことがあります。通常、これは、フラグメントが自分のホスト アクティビティとやり取りするのに必要なあらゆる手段を抽象化したインターフェースを定義し、さらに、そのインターフェースをホスト アクティビティに実装することで可能になります:

たとえば、News Reader アプリの HeadlinesFragment は、まさにそのようになっています:

public class HeadlinesFragment extends ListFragment {
    ...
    OnHeadlineSelectedListener mHeadlineSelectedListener = null;

    /* Must be implemented by host activity */
    public interface OnHeadlineSelectedListener {
        public void onHeadlineSelected(int index);
    }
    ...

    public void setOnHeadlineSelectedListener(OnHeadlineSelectedListener listener) {
        mHeadlineSelectedListener = listener;
    }
}

これにより、ユーザーが見出しを選択すると、フラグメントは以下のように(特定のハードコーディングされたアクティビティに通知するのではなく)ホスト アクティビティが指定したリスナーに通知します:

public class HeadlinesFragment extends ListFragment {
    ...
    @Override
    public void onItemClick(AdapterView<?> parent, 
                            View view, int position, long id) {
        if (null != mHeadlineSelectedListener) {
            mHeadlineSelectedListener.onHeadlineSelected(position);
        }
    }
    ...
}

このテクニックについては、タブレットと携帯端末のサポートで詳しく説明されています。

画面設定の変更を処理する

インターフェースの各パーツを実装するのに個別のアクティビティを使用している場合、インターフェースの一貫性を維持するために、(向きの変更などの)特定の設定変更に対応できるように注意する必要があります。

たとえば、Android 3.0 以上が動作する一般的な 7 インチ タブレットでは、News Reader サンプルがニュース記事を表示する場合、縦表示では個別のアクティビティを使用しますが、横表示では 2 ペイン レイアウトを使用します。

つまり、縦表示のときに記事閲覧用アクティビティが画面上にある場合、画面の向きが横方向に変わったことを検出したら、コンテンツを 2 ペイン レイアウトで表示するために、そのアクティビティを終了してメインのアクティビティに戻り、適切に応答しなければなりません:

public class ArticleActivity extends FragmentActivity {
    int mCatIndex, mArtIndex;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mCatIndex = getIntent().getExtras().getInt("catIndex", 0);
        mArtIndex = getIntent().getExtras().getInt("artIndex", 0);

        // If should be in two-pane mode, finish to return to main activity
        if (getResources().getBoolean(R.bool.has_two_panes)) {
            finish();
            return;
        }
        ...
}