Fragmentを使ったスマートフォン/タブレット向けUI両対応の方法


Android 3.0以降でタブレット向けに追加されたFragmentが、Android 4.0でスマートフォンでも利用出来るように拡張されました。
今まではTabletの画面構成の一部をフラグメント化(断片化)し、再利用に利用することが多かったと思われますが、
スマートフォン/タブレット共通のプラットフォームとなったことでUIの両対応に利用できるようになりました。

本エントリでは、リスト表示するアプリを例にスマートフォン、タブレットのUI両対応を行います。
フラグメントをつかうことでスマホ/タブレットで共通パーツをつかって効率的な開発をすることができます。


■図1:スマートフォンとタブレットのList表示と詳細表示

上記の図は、List(緑色パーツ)と詳細表示(オレンジパーツ)の2つのUI部品を用意し、
タブレットでは2カラムで同時に表示しています。
スマートフォンでは画面が狭いため、緑→オレンジの順で1つずつ見せています。
通常なら別々にActivityを用意するところですが、フラグメントを利用することで、
緑(List)とオレンジ(詳細表示)は共通パーツとして作成することができます。

それではつづきで紹介していきます。

画面サイズのチェック

UIを切り替えるためには、アプリケーションの動作する端末がスマートフォンかタブレットか知る必要があります。
スマートフォンとタブレットの画面サイズを判断する方法として、本エントリのサンプルでは「res/values/bools.xml」を利用します。

Androidアプリケーションは、res以下のディレクトリは画面解像度毎に細かく分類することができます。
例えば、drawable-mdpiなら160dpの端末、drawable-hdpiなら240dpの端末、と実行環境で使用するリソースをOSが自動で切り替えます。
これを利用し、values/bools.xmlにスマートフォンである設定を、values-sw600dp(横方向の解像度が600dp以上)にタブレットの設定を設けることで、サイズチェックに利用します。
※この値は、Android-4.0のソースコードがSplitActionBarの設定ON/OFFに使用している閾値です


※追記 2012/01/28
values-w480dpは、端末のLandscape(横向き)/Portrait(縦向き)に依存します。
values-w480dpにタブレットの設定を設けた場合、Galaxy Nexusでタブレット判定がされることをご指摘いただきました。
(SplitActionBarはLandscapeの場合OFFになる仕様のようです。)

Androidでは端末の縦横の状態に依存しないvaluesディレクトリとして、values-sw600dp(解像度は任意)があります。
values-sw600dpにbools.xmlを置く事で縦横の状態に依存しない設定とすることができます。
参考リンク:Android Developers 「Supporting Multiple Screens

■res/values/bools.xml

<resources>
    <bool name="is_tablet">false</bool>
</resources>

■res/values-sw480dp/bools.xml

<resources>
    <bool name="is_tablet">true</bool>
</resources>

上記設定bools.xmlの値を読み込む為のメソッドを用意し、簡易にタブレット/スマートフォンを判別できるように
メソッドを追加しておきます。
本メソッドを呼び出すと、OSが実行環境の解像度に合わせて(横方向の解像度480dp以上でタブレット)、設定値をよみだしてくれます。

■src/MultiFragmentSampleActivity.java

    private boolean isTabletMode(){
    	return getResources().getBoolean(R.bool.is_tablet);
    }

UIの切り替え

サンプルとして作成する、図1の構成は以下の通りです。

    スマートフォンのUI:「リストを選択すると次画面にその詳細が表示」
    タブレットのUI:「左ペインのリストを選択すると右ペインに詳細を表示」

このような画面構成の場合、共通している項目が「リスト」「詳細表示」の項目です。
エントリ冒頭でも触れましたが、共通している項目をFragmentを利用し、部品化しておくことで開発を効率化することができます。
共通項目を部品化することで、タブレット/スマートフォンで部品の表示方法を変更するだけで両対応を行うことができます。

本エントリでは、リスト表示にSampleListFragmentクラスを、詳細表示にSampleDetailFragmentクラスを作成しました。
これらは、アプリケーション起動時に呼び出されるActivityで以下の様に呼び出しています。
例としてSampleListFragmentの呼び出し時のコードを引用します。

■src/MultiFragmentSampleActivity.java

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

        // tablet/phoneで読み込むlayoutを変更する
        if(isTabletMode()){
        	setContentView(R.layout.main_tab);
        }else{
        	setContentView(R.layout.main_sp);
        }

    }

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

		//listの作成
		SampleListFragment listFragment = new SampleListFragment();

		//fragmentを配置する
		FragmentTransaction ft = getFragmentManager().beginTransaction();
		if(isTabletMode()){
			ft.add(R.id.list_container,listFragment);
		}else{
			ft.add(R.id.phone_container,listFragment);
		}
		ft.commit();

    }

Fragmentを追加する場所の構成が異なるため、
onCreate時に読み込む、Activityのレイアウトをタブレットの場合とスマートフォンの場合で変更しています。

22行目〜28行目において、FragmentをActivityに追加しています。
タブレットの判定を行うメソッドを利用し、タブレットの場合とスマートフォンの場合で追加する場所を変動させています。

ActivityのイベントをFragmentに通知する

Fragmentを利用する場合に、Fragment内で発生したボタンクリックなどのイベントをActivityに通知したいことが多々あります。
しかし、FragmentのAPIにはActivityにメッセージを通知するAPIは存在しません。
その為、Fragmentにリスナーを実装し、Activityでリスナー登録することでイベントを通知することができます。

TechBoosterでは「独自リスナーを作成する」でより汎用的な内容を紹介していますので、詳細な実装方法はそちらの記事をご覧になってください。

本エントリのサンプルでは、SampleListFragmentのアイテムクリックイベントをActivityに通知するListenerを作成し、
通知されたActivityでSampleDetailFragmentの読み込みに利用しています。

■src/SampleListFragment

public class SampleListFragment extends ListFragment {
	private onFragmentListClickedListener listener;

	......省略

	@Override
	public void onListItemClick(ListView l, View v, int position, long id) {
		// TODO Auto-generated method stub
		super.onListItemClick(l, v, position, id);
		listener.onFragmentListClick(rows[position]);
	}

	/**
	 * ListのClick情報を通知するListener
	 *
	 */
	public interface onFragmentListClickedListener {
		public void onFragmentListClick(String select);
	}

	/**
	 * Interfaceを登録する
	 *
	 */
	@Override
	public void onAttach(Activity activity) {
		super.onAttach(activity);
		listener = (onFragmentListClickedListener)activity;
	}

}

18行目でListenerを作成、26行目でListenerを登録しています。
Listenerを登録する場合に、Activityに該当するListenerがimplementされていない場合には、エラーとなってしまうため注意が必要です

10行目でListenerを呼び出しています。
SampleListFragmentでクリックされたアイテムの情報をSampleDetailFragmentに通知するため、
クリックされたアイテム情報を引数にしています。

次に、Activityに用意したListenerの実体のサンプルです。
Listenerは、Fragment内のListViewを選択した時に呼び出され、詳細表示のFragmentを読み込みます。
ただし、例に習いタブレット/スマートフォンでの両対応を行う為に、Fragmentの読み込み部分は
bools.xmlの値をチェックし動的切り換え可能な作りとしています。

■src/MultiFragmentSampleActivity.java

public class MultiFragmentSampleActivity extends Activity implements onFragmentListClickedListener{
	......省略

    /**
     * SampleListFragmentでListClickされるとよびだされる
     * ここでdetailFragmentを呼び出す
     */
	@Override
	public void onFragmentListClick(String select) {
		//detailFragmentの作成
		SampleDetailFragment detail = new SampleDetailFragment(select);

		//fragmentを配置する
		FragmentTransaction ft = getFragmentManager().beginTransaction();
		if(isTabletMode()){
			ft.add(R.id.detail_container,detail);
		}else{
			ft.add(R.id.phone_container,detail);
		}
		ft.addToBackStack(null);
		ft.commit();
	}

1行目でActivityにSampleListFragmentで作成したinterfaceをimplementしています。
繰り返しになりますが、忘れるとエラーになるため注意しましょう。

9行目以降がSampleListFragmentでアイテムが押下された時に呼び出されるメソッドです。
このメソッドの中で、SampleListFragmentで押下した情報を元にSampleDetailFragmentを読み込んでいます。

最後にサンプルの実行スクリーンショットを掲載します。
タブレット環境での実行結果は以下図の通りです。
左ペインで押下した内容が右ペインに表示される構成になっています。

■図2:タブレットでのサンプル実行結果

スマートフォン環境での実行結果は以下図の通りです。
図3(リスト画面)で選択した内容が図4(詳細画面)に反映されています。

■図3:スマートフォンでのサンプル実行結果(List)


■図4:スマートフォンでのサンプル実行結果(詳細画面)