2011年12月7日水曜日

FrameLayoutの利用方法まとめ

誰が言ったか忘れたけれど、スマフォのUIは3次元で考えようみたいなスライドを見かけた気がする。
「あにすと」も1画面で表示したい情報が日に日に増えてきたので一旦整理しておこうなまとめ。

※最近ですます調でのブログ更新に違和感が出てきたので、かるーく書いてきます。

参考

FrameLayout | Android Developers <http://developer.android.com/reference/android/widget/FrameLayout.html>

UIコンポーネント/FrameLayout - Android Wiki*
<http://wikiwiki.jp/android/?UI%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8%2FFrameLayout>

レイアウト(7)-FrameLayout ウィジェットを重ね合わせて配置する。 - 愚鈍人 <http://ichitcltk.hustle.ne.jp/gudon/modules/pico_rd/index.php?content_id=110>

Viewのフリックを実装する(一部問題あり) - タイトルは未定 <http://d.hatena.ne.jp/nakaji999/20110509/1304950487>

実例

・twicca

image image
タイムライン表示での下部アイコンメニュー(左)
ツイート選択削除時の上部メニュー(右)
※画像は公式サイトより借用しております。→twicca - twitter client for android <http://twicca.r246.jp/>

・2chMate

image image
レス選択時の上部関連レス抽出表示(左)
シークバー移動時の下部メニュー(右)

・TV番組表 - Android マーケット <https://market.android.com/details?id=com.atrtv.android.tvlist>

image 
番組選択時のダイアログ風メニュー(中央)

FrameLayoutを使うシチュエーション

  • 上位レイヤーのフレームを半透明とし、下位レイヤーのフレームを広く見せる(twicca)
  • 頻繁に利用する項目を常時表示とする(twicca)
  • 選択項目と関連付けた操作であることを意識付ける(twicca,2chMate)
  • 元の画面全体への動的な操作(2chMate)
  • 標準のダイアログ実装に憎しみが募ったら(TV番組表)

 

利点

※主に画面遷移ではないことに由来するもの

  • 描画が早い
  • onPause→onRestart→onResumeのアクティビティライフサイクルが発生しない
  • ことによりリソース破棄フェーズが発生しない
  • ことによりたとえばStream接続中に破棄しなくて済む
  • onResultActivityとか実装しなくていい
  • 元の画面表示と関連性を持たせたUIが作れる(選択したツイートをみせつつ→削除)
  • 半透明とすることで狭いスマフォ画面を広く使える(2段ベッド的な発想で)
  • シャレオツ(もしくはハッタリ)

 

欠点

※主に画面遷移ではないことに由来するもの

  • 実装が元画面と分断できないため、各フレーム状態の意識が必要(パスタ化)
  • AndroidUI慣れしているほど上位フレームを閉じるために戻るボタンを押して前画面に戻ってしまいアアーッとかなって作者への憎しみが募る(冤罪)

※アアーッってならないようにonKeyDown→KeyEvent.KEYCODE_BACKを拾って判断することは可能だけれども、アプリ全体での統一感が必要。

実装のポイント

  • 下位レイヤーと上位レイヤーのタッチイベント制御の仕様決定(タッチイベントを透過するか否か)
  • 上位レイヤーの動的表示時は突然出現させるとダサいダサいと中傷されるので、移動・透過などのアニメーションをつける
  • 上位レイヤーの動的表示時に、どの程度の領域とするか仕様決定(一部=twicca、10%~80%=2chMate、100%=TV番組表)

 

実装例

構成物概要

  • main.xml:レイアウトXML
  • anim\translate_xxx_in.xml:非表示→表示時の移動アニメーション
  • anim\translate_xxx_out.xml:表示→非表示時の移動アニメーション
  • FrameLayoutSampleActivity.java:初期起動Activitiy、全てこれでやる

ソース貼り付け

main.xml

  1: <?xml version="1.0" encoding="utf-8"?>
  2: <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3:   android:layout_width="fill_parent"
  4:   android:layout_height="fill_parent"
  5:   android:orientation="vertical" >
  6:   <FrameLayout
  7:     android:layout_width="fill_parent"
  8:     android:layout_height="fill_parent"
  9:     android:background="#FF808080"
 10:     >
 11:       <!-- FrameA -->
 12:     <LinearLayout
 13:         android:id="@+id/LayoutA"
 14:       android:layout_width="fill_parent"
 15:       android:layout_height="fill_parent"
 16:       android:orientation="vertical">
 17:       <Button
 18:         android:id="@+id/buttonA_B"
 19:         android:layout_width="fill_parent"
 20:         android:layout_height="0dip"
 21:         android:layout_weight="1"
 22:         android:text="A_B"
 23:         />
 24:       <Button
 25:         android:id="@+id/buttonA_C"
 26:         android:layout_width="fill_parent"
 27:         android:layout_height="0dip"
 28:         android:layout_weight="1"
 29:         android:text="A_C"
 30:         />
 31:       <TextView
 32:         android:layout_width="fill_parent"
 33:         android:layout_height="0dip"
 34:         android:layout_weight="1"
 35:         android:gravity="center"
 36:         android:text="FrameA" />
 37:       <Button
 38:         android:id="@+id/buttonA_D"
 39:         android:layout_width="fill_parent"
 40:         android:layout_height="0dip"
 41:         android:layout_weight="1"
 42:         android:text="A_D"
 43:         />
 44:       <Button
 45:         android:id="@+id/buttonA_E"
 46:         android:layout_width="fill_parent"
 47:         android:layout_height="0dip"
 48:         android:layout_weight="1"
 49:         android:text="A_E"
 50:         />
 51:       <Button
 52:         android:id="@+id/buttonA_F"
 53:         android:layout_width="fill_parent"
 54:         android:layout_height="0dip"
 55:         android:layout_weight="1"
 56:         android:text="A_F"
 57:         />
 58: 
 59:     </LinearLayout>
 60:       <!-- FrameB -->
 61:     <LinearLayout
 62:         android:id="@+id/LayoutB"
 63:       android:layout_width="fill_parent"
 64:       android:layout_height="50dip"
 65:       android:orientation="horizontal"
 66:       android:layout_gravity="bottom"
 67:       android:background="#80FF0000"
 68:       android:gravity="center"
 69:       >
 70:       <Button
 71:         android:id="@+id/buttonB_1"
 72:         android:layout_width="100dip"
 73:         android:layout_height="40dip"
 74:         android:text="B_1"
 75:         />
 76:       <TextView
 77:         android:layout_width="80dip"
 78:         android:layout_height="wrap_content"
 79:         android:gravity="center"
 80:         android:text="FrameB" />
 81:       <Button
 82:         android:id="@+id/buttonB_2"
 83:         android:layout_width="100dip"
 84:         android:layout_height="40dip"
 85:         android:text="B_2"
 86:         />
 87:     </LinearLayout>
 88: 
 89:     <!-- FrameC -->
 90:     <LinearLayout
 91:         android:id="@+id/LayoutC"
 92:       android:layout_width="fill_parent"
 93:       android:layout_height="50dip"
 94:       android:orientation="horizontal"
 95:       android:layout_gravity="top"
 96:       android:background="#8000FF00"
 97:       android:gravity="center"
 98:       >
 99:       <Button
100:         android:id="@+id/buttonC_1"
101:         android:layout_width="100dip"
102:         android:layout_height="40dip"
103:         android:text="C_1"
104:         />
105:       <TextView
106:         android:layout_width="80dip"
107:         android:layout_height="wrap_content"
108:         android:gravity="center"
109:         android:text="FrameC" />
110:       <Button
111:         android:id="@+id/buttonC_2"
112:         android:layout_width="100dip"
113:         android:layout_height="40dip"
114:         android:text="C_2"
115:         />
116:     </LinearLayout>
117: 
118:     <!-- FrameD -->
119:     <LinearLayout
120:         android:id="@+id/LayoutD"
121:       android:layout_width="wrap_content"
122:       android:layout_height="fill_parent"
123:       android:orientation="vertical"
124:       android:layout_gravity="left"
125:       android:background="#800000FF"
126:       android:gravity="center"
127:       >
128:       <Button
129:         android:id="@+id/buttonD_1"
130:         android:layout_width="80dip"
131:         android:layout_height="40dip"
132:         android:text="D_1"
133:         />
134:       <TextView
135:         android:layout_width="80dip"
136:         android:layout_height="wrap_content"
137:         android:gravity="center"
138:         android:text="FrameD" />
139:       <Button
140:         android:id="@+id/buttonD_2"
141:         android:layout_width="80dip"
142:         android:layout_height="40dip"
143:         android:text="D_2"
144:         />
145:     </LinearLayout>
146:     <!-- FrameE -->
147:     <LinearLayout
148:         android:id="@+id/LayoutE"
149:       android:layout_width="wrap_content"
150:       android:layout_height="fill_parent"
151:       android:orientation="vertical"
152:       android:layout_gravity="right"
153:       android:background="#80FF00FF"
154:       android:gravity="center"
155:       >
156:       <Button
157:         android:id="@+id/buttonE_1"
158:         android:layout_width="80dip"
159:         android:layout_height="40dip"
160:         android:text="E_1"
161:         />
162:       <TextView
163:         android:layout_width="80dip"
164:         android:layout_height="wrap_content"
165:         android:gravity="center"
166:         android:text="FrameE" />
167:       <Button
168:         android:id="@+id/buttonE_2"
169:         android:layout_width="80dip"
170:         android:layout_height="40dip"
171:         android:text="E_2"
172:         />
173:     </LinearLayout>
174:     <!-- FrameF -->
175:     <LinearLayout
176:         android:id="@+id/LayoutF"
177:       android:layout_width="fill_parent"
178:       android:layout_height="fill_parent"
179:       android:orientation="vertical"
180:       android:layout_gravity="top"
181:       android:background="#8000FFFF"
182:       android:gravity="center"
183:       >
184:       <Button
185:         android:id="@+id/buttonF_1"
186:         android:layout_width="100dip"
187:         android:layout_height="40dip"
188:         android:text="F_1"
189:         />
190:       <TextView
191:         android:layout_width="80dip"
192:         android:layout_height="wrap_content"
193:         android:gravity="center"
194:         android:text="FrameF" />
195:       <Button
196:         android:id="@+id/buttonF_2"
197:         android:layout_width="100dip"
198:         android:layout_height="40dip"
199:         android:text="F_2"
200:         />
201:     </LinearLayout>
202: 
203:   </FrameLayout>
204: </LinearLayout>

解説:FrameAを全画面、以降のFrameを上下左右に散らす


FrameLayoutSampleActivity.java

  1: package com.miquniqu.sample.framelayout;
  2: 
  3: import android.app.Activity;
  4: import android.os.Bundle;
  5: import android.view.MotionEvent;
  6: import android.view.View;
  7: import android.view.View.OnClickListener;
  8: import android.view.View.OnTouchListener;
  9: import android.view.animation.Animation;
 10: import android.view.animation.AnimationUtils;
 11: import android.widget.Button;
 12: import android.widget.LinearLayout;
 13: import android.widget.Toast;
 14: 
 15: public class FrameLayoutSampleActivity extends Activity {
 16: 
 17:   private LinearLayout mLayoutA = null;
 18:   private LinearLayout mLayoutB = null;
 19:   private LinearLayout mLayoutC = null;
 20:   private LinearLayout mLayoutD = null;
 21:   private LinearLayout mLayoutE = null;
 22:   private LinearLayout mLayoutF = null;
 23: 
 24:   private Button mButtonAB = null;
 25:   private Button mButtonAC = null;
 26:   private Button mButtonAD = null;
 27:   private Button mButtonAE = null;
 28:   private Button mButtonAF = null;
 29:   private Button mButtonB1 = null;
 30:   private Button mButtonB2 = null;
 31:   private Button mButtonC1 = null;
 32:   private Button mButtonC2 = null;
 33:   private Button mButtonD1 = null;
 34:   private Button mButtonD2 = null;
 35:   private Button mButtonE1 = null;
 36:   private Button mButtonE2 = null;
 37:   private Button mButtonF1 = null;
 38:   private Button mButtonF2 = null;
 39: 
 40:   Animation mAnimLeftIn = null;
 41:   Animation mAnimLeftOut = null;
 42: 
 43:   Animation mAnimRightIn = null;
 44:   Animation mAnimRightOut = null;
 45: 
 46:   Animation mAnimTopIn = null;
 47:   Animation mAnimTopOut = null;
 48: 
 49:   Animation mAnimBottomIn = null;
 50:   Animation mAnimBottomOut = null;
 51: 
 52:   /** Called when the activity is first created. */
 53:   @Override
 54:   public void onCreate(Bundle savedInstanceState) {
 55:     super.onCreate(savedInstanceState);
 56:     setContentView(R.layout.main);
 57: 
 58:     //Layoutを取得
 59:     mLayoutA = (LinearLayout) findViewById(R.id.LayoutA);
 60:     mLayoutB = (LinearLayout) findViewById(R.id.LayoutB);
 61:     mLayoutC = (LinearLayout) findViewById(R.id.LayoutC);
 62:     mLayoutD = (LinearLayout) findViewById(R.id.LayoutD);
 63:     mLayoutE = (LinearLayout) findViewById(R.id.LayoutE);
 64:     mLayoutF = (LinearLayout) findViewById(R.id.LayoutF);
 65:     //LayoutC~Fを非表示
 66:     mLayoutC.setVisibility(View.GONE);
 67:     mLayoutD.setVisibility(View.GONE);
 68:     mLayoutE.setVisibility(View.GONE);
 69:     mLayoutF.setVisibility(View.GONE);
 70: 
 71:     //Buttonを取得
 72:     mButtonAB = (Button) findViewById(R.id.buttonA_B);
 73:     mButtonAC = (Button) findViewById(R.id.buttonA_C);
 74:     mButtonAD = (Button) findViewById(R.id.buttonA_D);
 75:     mButtonAE = (Button) findViewById(R.id.buttonA_E);
 76:     mButtonAF = (Button) findViewById(R.id.buttonA_F);
 77:     mButtonB1 = (Button) findViewById(R.id.buttonB_1);
 78:     mButtonB2 = (Button) findViewById(R.id.buttonB_2);
 79:     mButtonC1 = (Button) findViewById(R.id.buttonC_1);
 80:     mButtonC2 = (Button) findViewById(R.id.buttonC_2);
 81:     mButtonD1 = (Button) findViewById(R.id.buttonD_1);
 82:     mButtonD2 = (Button) findViewById(R.id.buttonD_2);
 83:     mButtonE1 = (Button) findViewById(R.id.buttonE_1);
 84:     mButtonE2 = (Button) findViewById(R.id.buttonE_2);
 85:     mButtonF1 = (Button) findViewById(R.id.buttonF_1);
 86:     mButtonF2 = (Button) findViewById(R.id.buttonF_2);
 87: 
 88:     //ボタンのクリックリスナー
 89:     ClickListener clickListener = new ClickListener();
 90:     mButtonAB.setOnClickListener(clickListener);
 91:     mButtonAC.setOnClickListener(clickListener);
 92:     mButtonAD.setOnClickListener(clickListener);
 93:     mButtonAE.setOnClickListener(clickListener);
 94:     mButtonAF.setOnClickListener(clickListener);
 95:     mButtonB1.setOnClickListener(clickListener);
 96:     mButtonB2.setOnClickListener(clickListener);
 97:     mButtonC1.setOnClickListener(clickListener);
 98:     mButtonC2.setOnClickListener(clickListener);
 99:     mButtonD1.setOnClickListener(clickListener);
100:     mButtonD2.setOnClickListener(clickListener);
101:     mButtonE1.setOnClickListener(clickListener);
102:     mButtonE2.setOnClickListener(clickListener);
103:     mButtonF1.setOnClickListener(clickListener);
104:     mButtonF2.setOnClickListener(clickListener);
105: 
106:     //レイヤーのタッチキャンセルリスナー
107:     CancelTouchListener cancelListener = new CancelTouchListener();
108:     mLayoutF.setOnTouchListener(cancelListener);
109: 
110:     //アニメ
111:     mAnimLeftIn = AnimationUtils.loadAnimation(getApplicationContext(), R.anim.translate_left_in);
112:     mAnimLeftOut = AnimationUtils.loadAnimation(getApplicationContext(), R.anim.translate_left_out);
113:     mAnimRightIn = AnimationUtils.loadAnimation(getApplicationContext(), R.anim.translate_right_in);
114:     mAnimRightOut = AnimationUtils.loadAnimation(getApplicationContext(), R.anim.translate_right_out);
115:     mAnimTopIn = AnimationUtils.loadAnimation(getApplicationContext(), R.anim.translate_top_in);
116:     mAnimTopOut = AnimationUtils.loadAnimation(getApplicationContext(), R.anim.translate_top_out);
117:     mAnimBottomIn = AnimationUtils.loadAnimation(getApplicationContext(), R.anim.translate_bottom_in);
118:     mAnimBottomOut = AnimationUtils.loadAnimation(getApplicationContext(), R.anim.translate_bottom_out);
119: 
120:   }
121: 
122:   /***
123:    * 画面内のボタンクリックリスナー
124:    * 
125:    */
126:   public class ClickListener implements OnClickListener {
127:     @Override
128:     public void onClick(View v) {
129: 
130:       if (v instanceof Button) {
131:         Button button = (Button) v;
132:         Toast.makeText(FrameLayoutSampleActivity.this, button.getText(), Toast.LENGTH_SHORT).show();
133:       }
134: 
135:       //レイアウト表示有無
136:       if (v == mButtonAB || v == mButtonB1 || v == mButtonB2) {
137:         // 下
138:         cycleLayout(mLayoutB, mAnimBottomIn, mAnimBottomOut);
139:       } else if (v == mButtonAC || v == mButtonC1 || v == mButtonC2) {
140:         // 上
141:         cycleLayout(mLayoutC, mAnimTopIn, mAnimTopOut);
142:       } else if (v == mButtonAD || v == mButtonD1 || v == mButtonD2) {
143:         // 左
144:         cycleLayout(mLayoutD, mAnimLeftIn, mAnimLeftOut);
145:       } else if (v == mButtonAE || v == mButtonE1 || v == mButtonE2) {
146:         // 右
147:         cycleLayout(mLayoutE, mAnimRightIn, mAnimRightOut);
148:       } else if (v == mButtonAF || v == mButtonF1 || v == mButtonF2) {
149:         // ダイアログ風
150:         cycleLayout(mLayoutF, mAnimTopIn, mAnimTopOut);
151:       }
152: 
153:     }
154:   }
155: 
156:   /**
157:    * タッチ操作の取り消しをするリスナー
158:    */
159:   private class CancelTouchListener implements OnTouchListener {
160: 
161:     @Override
162:     public boolean onTouch(View v, MotionEvent event) {
163: 
164:       // このレイヤーで終了
165:       return true;
166:     }
167:   }
168: 
169:   /**
170:    * レイアウトVisible反転、アニメーション付
171:    * 
172:    * @param _target
173:    * @param _animIn
174:    * @param _animOut
175:    */
176:   public void cycleLayout(LinearLayout _target, Animation _animIn, Animation _animOut) {
177:     if (_target.getVisibility() == View.VISIBLE) {
178:       _target.setVisibility(View.GONE);
179:       _target.startAnimation(_animOut);
180:     } else {
181:       _target.setVisibility(View.VISIBLE);
182:       _target.startAnimation(_animIn);
183:     }
184: 
185:   }
186: }

解説:onCreateで不要なフレームを非表示、アニメーション取得。CancelTouchListenerは、フレームFから下位レイヤーにタッチイベントを渡さないためのもの


translate_bottom_in.xml

  1: <?xml version="1.0" encoding="utf-8"?>
  2: <set
  3:   xmlns:android="http://schemas.android.com/apk/res/android"
  4:   android:interpolator="@android:anim/accelerate_interpolator">
  5:   <translate
  6:     android:duration="1000"
  7:     android:fromXDelta="0%p" android:toXDelta="0%p"
  8:     android:fromYDelta="100%p" android:toYDelta="0%p"
  9:     android:fillAfter="true" android:fillEnabled="true"
 10:    />
 11: </set>

translate_bottom_out.xml

  1: <?xml version="1.0" encoding="utf-8"?>
  2: <set
  3:   xmlns:android="http://schemas.android.com/apk/res/android"
  4:   android:interpolator="@android:anim/accelerate_interpolator">
  5:   <translate
  6:     android:duration="1000"
  7:     android:fromXDelta="0%p" android:toXDelta="0%p"
  8:     android:fromYDelta="0%p" android:toYDelta="100%p"
  9:     android:fillAfter="true" android:fillEnabled="true"
 10:    />
 11: </set>
 12: 

 


以降省略、、、

  1: translate_left_in.xml
  2:   android:fromXDelta="-100%p" android:toXDelta="0%p"
  3:   android:fromYDelta="0%p" android:toYDelta="0%p"
  4: 
  5: translate_left_out.xml
  6:   android:fromXDelta="0%p" android:toXDelta="-100%p"
  7:   android:fromYDelta="0%p" android:toYDelta="0%p"
  8: 
  9: translate_right_in.xml
 10:   android:fromXDelta="100%p" android:toXDelta="0%p"
 11:   android:fromYDelta="0%p" android:toYDelta="0%p"
 12: 
 13: translate_right_out.xml
 14:   android:fromXDelta="0%p" android:toXDelta="100%p"
 15:   android:fromYDelta="0%p" android:toYDelta="0%p"
 16: 
 17: translate_top_in.xml
 18:   android:fromXDelta="0%p" android:toXDelta="0%p"
 19:   android:fromYDelta="-100%p" android:toYDelta="0%p"
 20: 
 21: translate_top_out.xml
 22:   android:fromXDelta="0%p" android:toXDelta="0%p"
 23:   android:fromYDelta="0%p" android:toYDelta="-100%p"
 24:     

 →(追記 2011/12/08)  「100%p」は対象Viewの親Viewの相対位置。「100%」は対象VIew相対位置を意味する。


実行結果例


Unable to display content. Adobe Flash is required.


上から順にボタンを選択していって、フレームFの際はイベントを透過させないため、ボタンで非表示としたキャプチャー。


上記はadakobaさん提供のツールでPC表示したものをJingでキャプチャーしたもの。感謝!



Android Screen Monitor - adakoda <http://www.adakoda.com/adakoda/android/asm/>


実行ファイル


GoogleドキュメントにFrameLayoutSample.apkファイルを公開したのでどうぞ。
ダウンロードできなかったらごめんなさい。


https://docs.google.com/open?id=0Bwy6je1r89t4ZGYyYWE3ZTMtZTNkYy00YjgxLWFkZGQtMGUxNDFkMmMyZjdm


 


メモ


2chMateの関連レス表示で80%ぐらいで止めた表示になるのは、フレーム内に透明のLayoutを配置して固定空間になるように制御しているような気がする。あと、フレーム表示中に戻るボタンでフレーム閉じる動作を入れているかも。


もさもさでも動画取れるようになったのは大収穫。ただの移動アニメーションが波のように段階的に移動してるのはカコイイけど、ただの遅延描画だぞ。


ボタン押してからやや反応が遅いのは、画面のサイズ100%分移動しているおかげで、無駄な空間を移動しているから。(初期位置が遠い)FrameFは全画面なので正しい。ちゃんとやるなら初期位置の調整が必要。
→(追記 2011/12/08) どうも「100%p」とした場合、対象Viewの親Viewの相対位置となるらしい。対象Viewの相対位置とするには「100%」と指定することで希望通りになる。

0 件のコメント:

コメントを投稿