2012/07/17

Android:エイリアスを使ったマルチスクリーン対応


画面サイズによって、レイアウトのパネル数を変化させるテクニックは有名です。
レイアウトパターンの1つMaster/Detailパターンは、タブレット等の大画面端末でよく
使われるパターンですが、ハンドセット端末では画面領域が限られている為、ほとんど使
われません。

今回は、もしあなたのアプリに下記の要求があった場合、
  • 画面サイズによってパネル数を変化させる必要がある
  • Android3.0との互換性も考える必要がある
どのようにレイアウトリソースを定義すればよいかを考えます。
(Android3.0より前のバージョンでも使えるテクニックです)

●リソース別名を使わない場合

まずは、画面領域が小さい端末(ハンドセット等)を対象にしたレイアウト。
これは1パネルレイアウトになります。
・res/layout/main.xml
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
    android:name="yuki.sample.ItemListFragment"
    android:id="@+id/item_list"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_marginLeft="16dp"
    android:layout_marginRight="16dp" />
次に、画面領域が大きい端末(タブレット等)を対象にしたレイアウト。
これは2パネルレイアウトになります。
・res/layout-sw600dp/main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_marginLeft="16dp"
    android:layout_marginRight="16dp"
    android:divider="?android:attr/dividerHorizontal"
    android:showDividers="middle">

    <fragment android:name="yuki.sample.ItemListFragment"
        android:id="@+id/item_list"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1" />

    <FrameLayout android:id="@+id/item_detail_container"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="3" />

</LinearLayout>
さらに、リソース修飾子sw<N>dpはAndroid3.2以降に追加されたため、
Android3.1以前をサポートするために抽象画面サイズのlarge版を用意します。
・res/layout-large/main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    .... />
    // 内容はres/layout-sw600dp/main.xmlと同じ
</LinearLayout>
今回用意したレイアウトファイルは次の3つです。
  • res/layout/main.xml (1パネル)
  • res/layout-sw600dp/main.xml (2パネル)
  • res/layout-large/main.xml (2パネル)

●リソース別名を使った場合

もし、sw600dpとlargeで用意したレイアウトファイルのような、レイアウト定義の重複を
避けたい場合、レイアウト別名ファイルを用意します。

例えば、次のような2ファイルを用意します。
  • res/layout/one_panels.xml (1パネル)
  • res/layout/two_panels.xml (2パネル)

そして、レイアウト別名を定義するファイルを用意します。
・res/values/layout.xml:
<resources>
    <item name="main" type="layout">@layout/one_panels</item>
</resources>
・res/values-sw600dp/layout.xml:
<resources>
    <item name="main" type="layout">@layout/two_panels</item>
</resources>
・res/values-large/layout.xml:
<resources>
    <item name="main" type="layout">@layout/two_panels</item>
</resources>
それぞれのファイルではレイアウト自体を定義していません。
レイアウトの別名を定義することでレイアウト定義の重複を避けています。
sw600dpの端末でレイアウト"main"を参照すると、結果的にres/layout/two_panels.xml
を参照することになります。
レイアウトを読み込むjavaコード側では、mainレイアウトリソースを読み込むだけで、
画面サイズにあった適切なレイアウトが取得できます。

画面サイズのみならず画面の向き(land or port)でもレイアウトを変化させるといった、
レイアウトリソースが爆発的に増えやすい仕様のアプリで有効な方法です。

●現在のレイアウトは1パネルか?2パネルか?

javaコード側で、現在のレイアウトが1パネルか2パネルかを判断する方法について考えます。
まず思い浮かぶのは下記でしょう。
if (findViewById(R.id.item_detail_container) != null) {
    // 2パネルモード
} else {
    // 1パネルモード
}
2パネルレイアウトでのみ定義しているitem_detail_containerが見つかれば2パネルモー
ドであると判断します。

しかし、この方法だとjavaコード側がレイアウトの詳細を知っていることになります。
さらに、レイアウト定義側もitem_detail_containerの扱いに注意が必要です。
この厄介な問題を解決するには、2パネルかどうかの判断フラグをリソースとして定義す
る方法があります。
・res/values/layout.xml:
<resources>
    <item name="main" type="layout">@layout/one_panels</item>
    <bool name="has_multi_panels">false</bool>
</resources>
・res/values-sw600dp/layout.xml:
<resources>
    <item name="main" type="layout">@layout/two_panels</item>
    <bool name="has_multi_panels">true</bool>
</resources>
javaコード側ではhas_multi_panelsのboolリソースを参照してパネル数を判断します。
こうすることで、javaコードとレイアウトリソースそれぞれの結合度を低く保つことが
できます。

より詳しい情報は下記を参照。
http://developer.android.com/training/multiscreen/screensizes.html

以上です。