2012年4月26日木曜日

Android Fragment + ViewPager には id が必要

ViewPager を Fragment と一緒に使うときの注意として ViewPager には id がセットされていなければならない、というものがあります。

例えば、次の用に XML レイアウトを使わないで ViewPager を画面に追加した場合、アプリが強制終了します。

  1. public class MainActivity2 extends FragmentActivity {  
  2.   
  3.     @Override  
  4.     public void onCreate(Bundle savedInstanceState) {  
  5.         super.onCreate(savedInstanceState);  
  6.   
  7.         ViewPager mViewPager = new ViewPager(this);  
  8.         setContentView(mViewPager);  
  9.   
  10.         PagerAdapter mAdapter = new PagerAdapter(this);  
  11.         mViewPager.setAdapter(mAdapter);  
  12.   
  13.     }  
  14.   
  15.     class PagerAdapter extends FragmentPagerAdapter {  
  16.   
  17.         private final Context mContext;  
  18.   
  19.         public PagerAdapter(FragmentActivity activity) {  
  20.             super(activity.getSupportFragmentManager());  
  21.             mContext = activity;  
  22.         }  
  23.   
  24.         @Override  
  25.         public int getCount() {  
  26.             return 5;  
  27.         }  
  28.   
  29.         @Override  
  30.         public Fragment getItem(int position) {  
  31.             Bundle args = new Bundle();  
  32.             args.putInt("num", position * position);  
  33.             return Fragment.instantiate(mContext,  
  34.                     SimpleFragment.class.getName(), args);  
  35.         }  
  36.     }  
  37. }  


  1. public class SimpleFragment extends Fragment {  
  2.   
  3.     @Override  
  4.     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {  
  5.         TextView tv = new TextView(inflater.getContext());  
  6.         String message = "Message : " + getArguments().getInt("num");  
  7.         tv.setText(message);  
  8.         return tv;  
  9.     }      
  10. }  


次のように IllegalArgumentException が投げられるのですが、

04-26 20:56:16.739: E/AndroidRuntime(28770): java.lang.IllegalArgumentException: No view found for id 0xffffffff for fragment SimpleFragment{4183dd90 #0 id=0xffffffff android:switcher:-1:0}
04-26 20:56:16.739: E/AndroidRuntime(28770): at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:864)

ここの部分のコードを見ると次のようになっています。

  1. 712     void moveToState(Fragment f, int newState, int transit, int transitionStyle) {  
  2.   
  3. 722         if (f.mState < newState) {  
  4.   
  5. 737             switch (f.mState) {  
  6.   
  7. 781                 case Fragment.CREATED:  
  8. 782                     if (newState > Fragment.CREATED) {  
  9. 784                         if (!f.mFromLayout) {  
  10. 785                             ViewGroup container = null;  
  11. 786                             if (f.mContainerId != 0) {  
  12. 787                                 container = (ViewGroup)mActivity.findViewById(f.mContainerId);  
  13. 788                                 if (container == null && !f.mRestored) {  
  14. 789                                     throw new IllegalArgumentException("No view found for id 0x"  
  15. 790                                             + Integer.toHexString(f.mContainerId)  
  16. 791                                             + " for fragment " + f);  
  17. 792                                 }  
  18. 793                             }  


これをみると、 Fragment (この場合は ViewPager の中身に Fragment のこと)がレイアウトから生成されていない場合で、Fragment の コンテナの Id が 0 でない場合に、その Id に対応する View が見つからないと上記のエラーの IllegalArgumentException が発行されることがわかります。

上記のエラーメッセージでは Fragment のコンテナの Id として 0 ではなく、0xffffffff が渡されていることがわかります。

ViewPager に明示的に Id をセットするとこのエラーはなくなります。

  1. @Override  
  2. public void onCreate(Bundle savedInstanceState) {  
  3.     super.onCreate(savedInstanceState);  
  4.   
  5.     ViewPager mViewPager = new ViewPager(this);  
  6.     setContentView(mViewPager);  
  7.     mViewPager.setId(100);  
  8.   
  9.     PagerAdapter mAdapter = new PagerAdapter(this);  
  10.     mViewPager.setAdapter(mAdapter);  
  11. }  


ここでは適当に 100 としましたが、こうするのは良くないです。

次のように res で id を定義し、それを使います。

  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <resources>  
  3.   
  4.     <item name="viewpager" type="id"/>  
  5.   
  6. </resources>  


  1. @Override  
  2. public void onCreate(Bundle savedInstanceState) {  
  3.     super.onCreate(savedInstanceState);  
  4.   
  5.     ViewPager mViewPager = new ViewPager(this);  
  6.     setContentView(mViewPager);  
  7.     mViewPager.setId(R.id.viewpager);  
  8.   
  9.     PagerAdapter mAdapter = new PagerAdapter(this);  
  10.     mViewPager.setAdapter(mAdapter);  
  11. }  

2012年4月20日金曜日

Android Up の振る舞いパターンを実装する

Android Design での Up と Back のガイドライン の振る舞いのパターンの実装方法を紹介します。

まず、Up と Back はそれぞれ次のように使い分けます。

・Up : 画面間の階層関係に基づいたアプリ内のナビゲーションに使う
・Back : 最近行った操作を逆時系列順にさかのぼるナビゲーションに使う

そのため、次のような違いがあります。

遷移範囲
・Up : アプリ内の遷移だけ
・Back : アプリ内だけでなく別のアプリやホームアプリにも遷移する

振る舞い
・Up : 画面遷移だけ
・Back : 画面遷移の他に、フローティングウィンドウ(ダイアログやポップアップ)のキャンセル、Action Mode のキャンセルや選択中のアイテムのキャンセル、ソフトキーボードを隠すなどの操作にも使われる


基本的には、Up は一つ上の階層に戻るナビゲーションに使います。


http://developer.android.com/design/media/navigation_up_vs_back_gmail.png

Up はアプリ内の遷移にだけ使うので、アプリのホーム画面には Up ボタンは置きません。



アプリ内のナビゲーション


・同じアプリの複数画面から遷移される画面での Up ボタンは、前の画面に戻るようにする

例えば、設定画面は同じアプリのいろいろな画面から遷移できるようになっていることが多いです。このようなエントリポイントがたくさんある画面では、Up ボタンが押されたときに前の画面に戻るようにします。つまり Back ボタンと同じ振る舞いです。

実装としては、finish() が適当でしょう。
  1. @Override  
  2. public boolean onOptionsItemSelected(MenuItem item) {  
  3.       
  4.     switch(item.getItemId()) {  
  5.         case android.R.id.home:  
  6.             finish();  
  7.             return true;  
  8.     }  
  9.       
  10.     return super.onOptionsItemSelected(item);  
  11. }  


・画面内での View の切り替えでは、Up や Back の振る舞いを変えない

例えば

  - タブやスワイプによる View の切り替え
  - ドロップダウンによる View の切り替え
  - リストのフィルタリング
  - リストのソート
  - ズームなどによる表示切り替え

ではアプリの階層は変わらず、Back 用のナビゲーション履歴も増えません。そのため、Up のナビゲーションをこの切り替えによって変更することはしません。



・同じ Activity 内での連続する画面の切り替えでは、Up や Back の振る舞いを変えない

Gmail アプリのように、リスト画面からその詳細画面に遷移し、そこからスワイプで前後のリストアイテムの詳細画面に遷移する場合も、アプリの階層は変わらず Back 用のナビゲーション履歴も増えません。

http://developer.android.com/design/media/navigation_between_siblings_gmail.png

この場合、詳細画面上の Up ボタンでは 1階層上に戻るようにします。リスト画面からしか詳細画面に遷移しないのであれば finish() でもいいでしょう。
Intent.FLAG_ACTIVITY_CLEAR_TOP と Intent.FLAG_ACTIVITY_SINGLE_TOP を組み合わせてstartActivity() を使ってリスト画面を呼び出す方法もあります。

ConversationDetails
  1. @Override  
  2. public boolean onOptionsItemSelected(MenuItem item) {  
  3.       
  4.     switch(item.getItemId()) {  
  5.         case android.R.id.home:  
  6.             Intent intent = new Intent(this, ConversationList.class);  
  7.             intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP |   
  8.                             Intent.FLAG_ACTIVITY_SINGLE_TOP);  
  9.             startActivity(intent);  
  10.             return true;  
  11.     }  
  12.       
  13.     return super.onOptionsItemSelected(item);  
  14. }  

いずれもスタック上は
| ConversationList |
 ↓ 
| ConversationList   ConversationDetails |
 ↓ Up
| ConversationList |

という動作になります。



・同じ階層の別 Activity への切り替えでは、Up の振る舞いを変えない

http://developer.android.com/design/media/navigation_between_siblings_market1.png

別 Activity への遷移のため、Back 用のナビゲーション履歴が増えます。しかし、アプリの階層として同じ位置にあたる画面なので、Up ボタンの振る舞いは Back と異なり一つ上の階層に遷移するようにします。

スタック上の動作としては次のようになります。
| BookList | 
 ↓
| BookList  Book1Details |
 ↓    ↓ Up
 ↓   | BookList |
 ↓ 
| BookList  Book1Details  Book2Details |
 ↓ Up
| BookList |

この場合、Book2Details の Up の振る舞いとして finish() は使えません。いずれも Intent.FLAG_ACTIVITY_CLEAR_TOP と Intent.FLAG_ACTIVITY_SINGLE_TOP を組み合わせてstartActivity() を使ってリスト画面を呼び出す方法がいいでしょう。


Book1Details
Book2Details
  1. @Override  
  2. public boolean onOptionsItemSelected(MenuItem item) {  
  3.       
  4.     switch(item.getItemId()) {  
  5.         case android.R.id.home:  
  6.             Intent intent = new Intent(this, BookList.class);  
  7.             intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP |   
  8.                             Intent.FLAG_ACTIVITY_SINGLE_TOP);  
  9.             startActivity(intent);  
  10.             return true;  
  11.     }  
  12.       
  13.     return super.onOptionsItemSelected(item);  
  14. }  

アプリの内部構成が複数のカテゴリにわかれていて、あるカテゴリの下の階層の画面から別のカテゴリの下の階層に遷移した場合、遷移先の画面の Up の振る舞いはそのカテゴリ内での1つ上の階層に戻ることです。

http://developer.android.com/design/media/navigation_between_siblings_market2.png

実装としては、次のように BookDetails と MovieDetails で startActivity() 先を変えます。


Book1Details
Book2Details
  1. @Override  
  2. public boolean onOptionsItemSelected(MenuItem item) {  
  3.       
  4.     switch(item.getItemId()) {  
  5.         case android.R.id.home:  
  6.             Intent intent = new Intent(this, BookList.class);  
  7.             intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP |   
  8.                             Intent.FLAG_ACTIVITY_SINGLE_TOP);  
  9.             startActivity(intent);  
  10.             finish();  
  11.             return true;  
  12.     }  
  13.       
  14.     return super.onOptionsItemSelected(item);  
  15. }  

Movie1Details
  1. @Override  
  2. public boolean onOptionsItemSelected(MenuItem item) {  
  3.       
  4.     switch(item.getItemId()) {  
  5.         case android.R.id.home:  
  6.             Intent intent = new Intent(this, MovieList.class);  
  7.             intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP |   
  8.                             Intent.FLAG_ACTIVITY_SINGLE_TOP);  
  9.             startActivity(intent);  
  10.             finish();  
  11.             return true;  
  12.     }  
  13.       
  14.     return super.onOptionsItemSelected(item);  
  15. }  

スタック上の動作はこうなります。

| Top  BookList | 
 ↓
| Top  BookList  Book1Details |
 ↓    ↓ Up
 ↓   | Top  BookList |
 ↓ 
| Top  BookList  Book1Details  Book2Details |
 ↓    ↓ Up
 ↓   | Top  BookList |
 ↓ 
| Top  BookList  Book1Details  Book2Details  Movie1Details |
 ↓ Up
| Top  BookList  Book1Details  Book2Details  MovieList |



ホープアプリ上のウィジェットやノーティフィケーションからのナビゲーション

ウィジェットやノーティフィーケションから遷移した画面上の Up ボタンの振る舞いは、次のようにします。

 ・同じアプリの特定の画面にいるときにノーティフィーケションから遷移した場合は、その特定の画面に戻る
 ・それ以外は、アプリのホーム画面(トップ画面)に遷移する


http://developer.android.com/design/media/navigation_from_outside_back.png

タスクのバックスタックにホーム画面を挿入しておくことで、Back ボタンでアプリを抜ける際に、どうやって遷移先の画面に移動したのかを忘れているユーザーをホーム画面にナビゲートすることができます。

API Level 11 から追加された PendingIntent#getActivities(Context context, int requestCode, Intent[] intents, int flags) を使うと、スタックに複数の Activity を挿入した状態にすることができます。

例えば、
  1. public class WidgetProvider extends AppWidgetProvider {  
  2.   
  3.     @Override  
  4.     public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {  
  5.         super.onUpdate(context, appWidgetManager, appWidgetIds);  
  6.   
  7.         RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_layout);  
  8.         Intent[] intents = new Intent[2];  
  9.         intents[0] = new Intent(context, MainActivity.class);  
  10.         intents[1] = new Intent(context, MainActivity2.class);  
  11.         PendingIntent pendingIntent = PendingIntent.getActivities(context, 0, intents, Intent.FLAG_ACTIVITY_NEW_TASK);  
  12.         views.setOnClickPendingIntent(R.id.btn, pendingIntent);  
  13.           
  14.         appWidgetManager.updateAppWidget(appWidgetIds, views);  
  15.   
  16.     }  
  17. }  
のようにすると

| ホームアプリ |
 ↓
 ↓ ホーム画面のウィジェットの R.id.btn をタップ
 ↓
| ホームアプリ | MainActivity  MainActivity2 |

のようになります。

そのため、ここで Back ボタンを押すと MainActivity に遷移します。

MainActivity2 を
  1. @Override  
  2. public boolean onOptionsItemSelected(MenuItem item) {  
  3.       
  4.     switch(item.getItemId()) {  
  5.         case android.R.id.home:  
  6.             Toast.makeText(this"Hello", Toast.LENGTH_SHORT).show();  
  7.             Intent intent = new Intent(this, MainActivity.class);  
  8.             intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);  
  9.             startActivity(intent);  
  10.             finish();  
  11.             return true;  
  12.     }  
  13.       
  14.     return super.onOptionsItemSelected(item);  
  15. }  
としておけば、Up を押した場合も MainActivity に遷移するようになります。

ちなみに MainActivity, MainActivity2 両方とも launchMode="standard" で、すでにアプリを起動している状態からだと

| ホームアプリ | MainActivity |
 ↓
| ホームアプリ | MainActivity  MainActivity2 |
 ↓ Home ボタン
| MainActivity  MainActivity2 | ホームアプリ |
 ↓
 ↓ ホーム画面のウィジェットの R.id.btn をタップ
 ↓
| ホームアプリ | MainActivity  MainActivity2 | MainActivity(2)  MainActivity2(2) | 

のようなスタックになります。

MainActivity だけ launchMode="singleTask" にすると

| ホームアプリ | MainActivity |
 ↓
| ホームアプリ | MainActivity  MainActivity2 |
 ↓ Home ボタン
| MainActivity  MainActivity2 | ホームアプリ |
 ↓
 ↓ ホーム画面のウィジェットの R.id.btn をタップ
 ↓
| ホームアプリ | MainActivity  MainActivity2(2) | 

のように MainActivity2 は新しく作られ、そこから Back で戻った先の MainActivity は以前のインスタンスになります。



・インダイレクトノーティフィケーション

複数のイベントの情報を同時に提供するときに、単一のノーティフィケーションを通知して、そこから複数のイベントについてまとめた中間画面(interstitial screen)にユーザーを導き、そこからイベントに対する処理や対応するアプリ画面への遷移を提供するスタイルをインダイレクトノーティフィケーション(indirect notifications)といいます。

中間画面で Back ボタンが押されたときは、間によけいなスタックを入れずノーティフィケーションを呼び出した画面に戻るようにします。
中間画面からアプリの対応する画面に遷移したら、そこから Back や Up ボタンでは上記と同じようにアプリのホーム画面(トップ画面)を経由するようにします。中間画面には戻りません。

http://developer.android.com/design/media/navigation_indirect_notification.png



・ポップアップノーティフィケーション

電話がかかってきたときや、Gtalk でビデオチャットの招待がきたときなど、すぐに対応するべき通知はポップアップで表示されることがあります(すぐに対応しなければならない通知以外では使ってはいけない)。

ポップアップノーティフィケーションでの Up / Back の振る舞いはインダイレクトノーティフィケーションと同じような感じで、ポップアップが出たときに Back でキャンセルするとポップアップが消え、ポップアップからアプリに遷移した後は、Up と Back ボタンではアプリのホーム画面(トップ画面)を経由するようにします。

http://developer.android.com/design/media/navigation_popup_notification.png




アプリ間のナビゲーション

Android の大きな特徴として別のアプリの画面をあたかも自分のアプリの続きのように遷移することができる、という点があります。

アプリA から アプリB のある画面を呼び出してその結果を受け取りたい場合は、アプリB のある画面(の Activity)を アプリA のタスク内で起動しなければなりません。 例えば、launchMode="singleTask" の Activity を startActivityForResult() で起動した場合、すぐに RESULT_CANCELED が返ってきてしまいます。

例えば、共有をサポートするアプリを呼び出した場合、

http://developer.android.com/design/media/navigation_between_apps_inward.png

呼び出し先の Activity は呼び出し元と同じタスクになります。

呼び出し先で Back ボタンが押されたり、呼び出し先での処理が完了して finish() した場合は、呼び出し元の画面に戻ります。

http://developer.android.com/design/media/navigation_between_apps_back.png

一方、呼び出し先で Up ボタンが押された場合、ユーザーは呼び出し先のアプリに留まりたいということなので、新しいタスクとして呼び出し先のホーム画面(もしくは1階層上の画面)に遷移するようにします。そこからさらに Back ボタンでアプリを終了した場合は、呼び出し元のアプリではなくホームアプリに戻るようにします。

http://developer.android.com/design/media/navigation_between_apps_up.png

最初の呼び出し元のタスクはバックグラウンドで保持され、ユーザーは後で Recent apps から戻ることができます。すでに呼び出し先のアプリ自身のタスクが走っている状態だと、Up ボタンが押された場合は、そのタスクが新しいタスクに置き換えられます。

新しくタスクを起動し、それ以外のタスクをホームアプリのバックグラウンドに移すには、次のように Intent.FLAG_ACTIVITY_NEW_TASK と Intent.FLAG_ACTIVITY_TASK_ON_HOME を組み合わせます。
  1. @Override  
  2. public boolean onOptionsItemSelected(MenuItem item) {  
  3.       
  4.     switch(item.getItemId()) {  
  5.         case android.R.id.home:  
  6.             Toast.makeText(this"Hello", Toast.LENGTH_SHORT).show();  
  7.             Intent intent = new Intent(this, MainActivity.class);  
  8.             intent.setFlags(Intent.FLAG_ACTIVITY_TASK_ON_HOME | Intent.FLAG_ACTIVITY_NEW_TASK);  
  9.             startActivity(intent);  
  10.             finish();  
  11.             return true;  
  12.     }  
  13.       
  14.     return super.onOptionsItemSelected(item);  
  15. }  



2012年4月19日木曜日

Android Layout Cookbook と Android UI Cookbook for 4.0 の電子書籍がでました!



インプレスジャパンダイレクト ショッピング&サービス -

■キャンペーン期間(4/19~4/25)
・¥1,600 Android UI Cookbook for 4.0 ICS(Ice Cream Sandwich)アプリ開発術
・¥1,600 Android Layout Cookbook アプリの価値を高める開発テクニック
・¥3,000 2点セット販売

■キャンペーン終了後(4/26~)
・¥2,600 Android UI Cookbook for 4.0 ICS(Ice Cream Sandwich)アプリ開発術
・¥2,600 Android Layout Cookbook アプリの価値を高める開発テクニック

4/26 まで割引価格(紙の書籍の半額)です!

Android カスタム ViewGroup 用XMLのルートタグを <merge> にする

レイアウトXMLファイルから View を生成するときに LayoutInfalter の inflate() メソッドを使いますが、inflate() メソッドには引数が2つのものと3つのものがあります。 引数が2つのメソッドは、内部で inflate(resource, root, root != null) のように引数が3つのメソッドを呼んでいます。
この attachToRoot に true を指定した場合と false にした場合で返ってくるルートビューが異なります。

例えば
  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.   
  7.     <TextView  
  8.         android:layout_width="fill_parent"  
  9.         android:layout_height="wrap_content"  
  10.         android:text="@string/hello" />  
  11.   
  12. </LinearLayout>  
  1. LayoutInflater inflater = getLayoutInflater();  
  2. FrameLayout root = new FrameLayout(this);  
に対して
  1. View v = inflater.inflate(R.layout.main, root, true);  
とした場合は v は FrameLayout (つまり root)になります。

一方、
  1. View v = inflater.inflate(R.layout.main, root, false);  
または
  1. View v = inflater.inflate(R.layout.main, nulltrue);  
  2.   
  3. View v = inflater.inflate(R.layout.main, nullfalse);  
とした場合は v は LinearLayout (つまり R.layout.main のルートビュー)になります。

inflater.inflate(R.layout.main, root, false)



inflater.inflate(R.layout.main, null, false)

の違いは、root が null でない場合はそれに合うような LayoutParams が返ってくるルートビューにセットされるという点です。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/view/LayoutInflater.java#471
  1. 471                     if (root != null) {  
  2. 472                         if (DEBUG) {  
  3. 473                             System.out.println("Creating params from root: " +  
  4. 474                                     root);  
  5. 475                         }  
  6. 476                         // Create layout params that match root, if supplied  
  7. 477                         params = root.generateLayoutParams(attrs);  
  8. 478                         if (!attachToRoot) {  
  9. 479                             // Set the layout params for temp if we are not  
  10. 480                             // attaching. (If we are, we use addView, below)  
  11. 481                             temp.setLayoutParams(params);  
  12. 482                         }  
  13. 483                     }  
さて、この inflate() メソッドをみると、<merge> タグのチェックを行っている箇所があります。
  1. 424     public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {  
  2.   
  3. 453                 if (TAG_MERGE.equals(name)) {  
  4. 454                     if (root == null || !attachToRoot) {  
  5. 455                         throw new InflateException("<merge /> can be used only with a valid "  
  6. 456                                 + "ViewGroup root and attachToRoot=true");  
  7. 457                     }  
  8. 458   
  9. 459                     rInflate(parser, root, attrs, false);  
  10. 460                 } else {  
XML の最初のスタートタグが <merge> だった場合、root が null だったり attachToRoot が false だと InflateException が投げられます。

考えてみれば当たり前ですね。<merge> タグは addView されたときに親の View と合体するということなので、合体対象がいない場合戻り値の View として返すものがなくなってしまいます。



どういう場面で <merge> タグのレイアウトを inflate() することがあるかというとオリジナルの ViewGroup を作る場合です。

例えば、ボタンが縦に3つ並んだ LinearLayout をオリジナルの ViewGroup にしたいとします。

つまり、
  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.   
  7.     <TextView  
  8.         android:layout_width="fill_parent"  
  9.         android:layout_height="wrap_content"  
  10.         android:text="@string/hello" />  
  11.   
  12.     <LinearLayout  
  13.         android:layout_width="fill_parent"  
  14.         android:layout_height="wrap_content"  
  15.         android:orientation="vertical" >  
  16.   
  17.         <Button  
  18.             android:id="@+id/button1"  
  19.             android:layout_width="wrap_content"  
  20.             android:layout_height="wrap_content"  
  21.             android:text="Button" />  
  22.   
  23.         <Button  
  24.             android:id="@+id/button2"  
  25.             android:layout_width="wrap_content"  
  26.             android:layout_height="wrap_content"  
  27.             android:text="Button" />  
  28.   
  29.         <Button  
  30.             android:id="@+id/button3"  
  31.             android:layout_width="wrap_content"  
  32.             android:layout_height="wrap_content"  
  33.             android:text="Button" />  
  34.     </LinearLayout>  
  35.   
  36. </LinearLayout>  
  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.   
  7.     <TextView  
  8.         android:layout_width="fill_parent"  
  9.         android:layout_height="wrap_content"  
  10.         android:text="@string/hello" />  
  11.   
  12.     <yanzm.example.viewgroupmerge.MyViewGroup  
  13.         android:layout_width="fill_parent"  
  14.         android:layout_height="wrap_content"  
  15.         />  
  16.   
  17. </LinearLayout>  
にしたいということです。

そのためには、この MyViewGroup 自身に縦に並ぶボタン3つを持たせる必要があります。


初心者がやりがちなのがこういうコードです。
  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="wrap_content"  
  5.     android:orientation="vertical" >  
  6.   
  7.     <Button  
  8.         android:id="@+id/button1"  
  9.         android:layout_width="wrap_content"  
  10.         android:layout_height="wrap_content"  
  11.         android:text="Button" />  
  12.   
  13.     <Button  
  14.         android:id="@+id/button2"  
  15.         android:layout_width="wrap_content"  
  16.         android:layout_height="wrap_content"  
  17.         android:text="Button" />  
  18.   
  19.     <Button  
  20.         android:id="@+id/button3"  
  21.         android:layout_width="wrap_content"  
  22.         android:layout_height="wrap_content"  
  23.         android:text="Button" />  
  24.   
  25. </LinearLayout>  
  1. public class MyViewGroup extends FrameLayout {  
  2.   
  3.     public MyViewGroup(Context context) {  
  4.         super(context);  
  5.         init(context);  
  6.     }  
  7.   
  8.     public MyViewGroup(Context context, AttributeSet attrs) {  
  9.         super(context, attrs);  
  10.         init(context);  
  11.     }  
  12.   
  13.     public MyViewGroup(Context context, AttributeSet attrs, int defStyle) {  
  14.         super(context, attrs, defStyle);  
  15.         init(context);  
  16.     }  
  17.   
  18.     private void init(Context context) {  
  19.         LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);  
  20.         View v = inflater.inflate(R.layout.custom_layout, nullfalse);  
  21.         addView(v);  
  22.     }  
  23. }  
これでも動きますが、View 階層が一つ多くなってしまうのでよくありません。 そこで、<merge> タグを使って次のようにすると、元と同じ View 階層にとどめておけます。
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <merge xmlns:android="http://schemas.android.com/apk/res/android"  
  3.     android:layout_width="fill_parent"  
  4.     android:layout_height="wrap_content" >  
  5.   
  6.     <Button  
  7.         android:id="@+id/button1"  
  8.         android:layout_width="wrap_content"  
  9.         android:layout_height="wrap_content"  
  10.         android:text="Button" />  
  11.   
  12.     <Button  
  13.         android:id="@+id/button2"  
  14.         android:layout_width="wrap_content"  
  15.         android:layout_height="wrap_content"  
  16.         android:text="Button" />  
  17.   
  18.     <Button  
  19.         android:id="@+id/button3"  
  20.         android:layout_width="wrap_content"  
  21.         android:layout_height="wrap_content"  
  22.         android:text="Button" />  
  23.   
  24. </merge>  
  1. public class MyViewGroup extends LinearLayout {  
  2.   
  3.     public MyViewGroup(Context context) {  
  4.         super(context);  
  5.         init(context);  
  6.     }  
  7.   
  8.     public MyViewGroup(Context context, AttributeSet attrs) {  
  9.         super(context, attrs);  
  10.         init(context);  
  11.     }  
  12.   
  13.     public MyViewGroup(Context context, AttributeSet attrs, int defStyle) {  
  14.         super(context, attrs, defStyle);  
  15.         init(context);  
  16.     }  
  17.   
  18.     private void init(Context context) {  
  19.         setOrientation(LinearLayout.VERTICAL);  
  20.         LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);  
  21.         View v = inflater.inflate(R.layout.custom_layout, thistrue);  
  22.     }  
  23. }  
こうすれば <merge> 部分が MyViewGroup と合体してくれます。


さらに、View には inflate(Context context, int resource, ViewGroup root) という static メソッドがあり、 このメソッドを使ってさらに簡単に書けます。
  1. private void init(Context context) {  
  2.     setOrientation(LinearLayout.VERTICAL);  
  3.     View.inflate(context, R.layout.custom_layout, this);  
  4. }  
LayoutInflater のインスタンスを取得するとしては以下の方法をよく使います。


2012年4月18日水曜日

DartEditor の Dart Web Launch に Arguments がついた!

DartEditor の Dart Web Launch が

Version 0.1.0.201203261044, Build 5845 だと


だったのが

Version 0.1.0.201204121423, Build 6479 では


Arguments が増えてる!

これで何がうれしいかというと、DartEditor で less を使う で書いためんどいテクニックがいらないということです。
単にこの Arguments に --allow-file-access-from-files を入れればよくなったのでらくちん!



他にも

・Preferences に Enable AnalysisServer のチェックボックスが増えた



・Tools に Callers が増えて、Help にあった Welcome がここに移動した

・Force Recompile が Re-Analyze Sources に変わった



・Help に API Reference... が増えた






Android Fragment は破棄時に保持している View の状態を保存させている

前々回(Android Fragment で setArguments() してるサンプルが多いのはなぜ?)に Argument を使えば再生成時に値を引き継げることを書きました。
前回(Android レイアウトから生成した Fragment は FragmentTransaction の対象にしてはいけない)はレイアウトから生成した Fragment には setArguments() できないことを書きました。

では、レイアウトから生成した Fragment で再生成時に以前の状態を引き継ぐにはどうしたらいいのか、ということなんですが、Activity と同じように onSaveInstanceState(Bundle outState) 時に引数で渡される Bundle に入れておけば onCreate(Bundle), onCreateView(LayoutInflater, ViewGroup, Bundle), onActivityCreated(Bundle) のときに渡される Bundle から取り出すことができます。

自分の Fragment が持つ状態(例えばフィールドの値など)はこの方法で引き継ぐのが普通ですが、独自の View を使っていて、その View の状態(つまりカスタムビュークラスのフィールド値など)を引き継ぐには、View の onSaveInstanceState() で引き継ぎたい値を格納した Parcelable を返すのが普通です。

例えば、TextView では現在のカーソルの位置を、ListView では現在選択されているアイテムのIDなどを Parcelable として格納しています。

この View の onSaveInstanceState() は saveHierarchyState(SparseArray<Parcelable> container) をトリガーとして呼ばれます。

Fragment が破棄される部分のコード(#873以後)を見てみると

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/FragmentManager.java#874
  1.     712     void moveToState(Fragment f, int newState, int transit, int transitionStyle) {  
  2. ...  
  3.     722         if (f.mState < newState) {  
  4. ...  
  5.     781                 case Fragment.CREATED:  
  6.     782                     if (newState > Fragment.CREATED) {  
  7. ...  
  8.     819                         if (f.mView != null) {  
  9.     820                             f.restoreViewState();  
  10.     821                         }  
  11. ...  
  12.     850         } else if (f.mState > newState) {  
  13.     851             switch (f.mState) {  
  14. ...  
  15.     873                 case Fragment.STOPPED:  
  16.     874                 case Fragment.ACTIVITY_CREATED:  
  17.     875                     if (newState < Fragment.ACTIVITY_CREATED) {  
  18.     876                         if (DEBUG) Log.v(TAG, "movefrom ACTIVITY_CREATED: " + f);  
  19.     877                         if (f.mView != null) {  
  20.     878                             // Need to save the current view state if not  
  21.     879                             // done already.  
  22.     880                             if (!mActivity.isFinishing() && f.mSavedViewState == null) {  
  23.     881                                 saveFragmentViewState(f);  
  24.     882                             }  
  25.     883                         }  
Fragment が View を持っている場合は saveFragmentViewState() というメソッドを呼んでいます。

このメソッドでは

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/FragmentManager.java#1435
  1. 1435     void saveFragmentViewState(Fragment f) {  
  2. 1436         if (f.mView == null) {  
  3. 1437             return;  
  4. 1438         }  
  5. 1439         if (mStateArray == null) {  
  6. 1440             mStateArray = new SparseArray<Parcelable>();  
  7. 1441         } else {  
  8. 1442             mStateArray.clear();  
  9. 1443         }  
  10. 1444         f.mView.saveHierarchyState(mStateArray);  
  11. 1445         if (mStateArray.size() > 0) {  
  12. 1446             f.mSavedViewState = mStateArray;  
  13. 1447             mStateArray = null;  
  14. 1448         }  
  15. 1449     }  
このように、View の saveHierarchyState() が呼ばれていることがわかります。

View の saveHierarchyState() では

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/view/View.java#9787
  1. 9787     public void saveHierarchyState(SparseArray<Parcelable> container) {  
  2. 9788         dispatchSaveInstanceState(container);  
  3. 9789     }  
  1. 9802     protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {  
  2. 9803         if (mID != NO_ID && (mViewFlags & SAVE_DISABLED_MASK) == 0) {  
  3. 9804             mPrivateFlags &= ~SAVE_STATE_CALLED;  
  4. 9805             Parcelable state = onSaveInstanceState();  
  5. 9806             if ((mPrivateFlags & SAVE_STATE_CALLED) == 0) {  
  6. 9807                 throw new IllegalStateException(  
  7. 9808                         "Derived class did not call super.onSaveInstanceState()");  
  8. 9809             }  
  9. 9810             if (state != null) {  
  10. 9811                 // Log.i("View", "Freezing #" + Integer.toHexString(mID)  
  11. 9812                 // + ": " + state);  
  12. 9813                 container.put(mID, state);  
  13. 9814             }  
  14. 9815         }  
  15. 9816     }  
このように、onSaveInstanceState() を呼んで返ってきた Parcelable を引数の SparseArray に格納しています。

ViewGroup ではこの dispatchSaveInstanceState() が Override されており

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/view/ViewGroup.java#2294
  1.    2294     @Override  
  2.    2295     protected void dispatchSaveInstanceState(SparseArray<parcelable> container) {  
  3.    2296         super.dispatchSaveInstanceState(container);  
  4.    2297         final int count = mChildrenCount;  
  5.    2298         final View[] children = mChildren;  
  6.    2299         for (int i = 0; i < count; i++) {  
  7.    2300             View c = children[i];  
  8.    2301             if ((c.mViewFlags & PARENT_SAVE_DISABLED_MASK) != PARENT_SAVE_DISABLED) {  
  9.    2302                 c.dispatchSaveInstanceState(container);  
  10.    2303             }  
  11.    2304         }  
  12.    2305     }  
  13. </parcelable>  
自身だけでなく子 View の dispatchSaveInstanceState() も呼ぶようになっています。

onSavedInstanceState() で返した Parcelable は onRestoreInstanceState(Parcelable state) で取り出すことができます。このメソッドは restoreHierarchyState(SparseArray<Parcelable> container) をトリガーとして呼ばれます。そして、Fragment が生成されるときに呼ばれる restoreViewState() メソッド内で restoreHierarchyState() を呼んでいます。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/Fragment.java#586
  1. 586     final void restoreViewState() {  
  2. 587         if (mSavedViewState != null) {  
  3. 588             mView.restoreHierarchyState(mSavedViewState);  
  4. 589             mSavedViewState = null;  
  5. 590         }  
  6. 591     }  


つまり、カスタムビューを作る場合、その中だけで引き継ぎが完結する状態(private なフィールド値など)は View の onSaveInstanceState() を利用しましょう。そうすることで Fragment が View のフィールド値をわざわざ取り出して引き継ぐような依存性を回避できます。





2012年4月17日火曜日

Android レイアウトから生成した Fragment は FragmentTransaction の対象にしてはいけない

■ レイアウトから作成した Fragment には setArguments できない

前回のエントリで Fragment の Arguments の利点をいろいろ紹介しましたが、レイアウト内に <fragment> タグで定義して生成した Fragment には setArguments() をすることができません。

まず、Fragment.java のコードをみると Arguments を保持するフィールドである mArguments のコメントとして“生成時の引数である”と書いてあります。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/Fragment.java#370
  1. 370     // Construction arguments;  
  2. 371     Bundle mArguments;  
つまり、生成したあとの任意のタイミングでセットするようなものではない、ということです。

さらに、setArguments() の実装をみると

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/Fragment.java#652
  1. 659     public void setArguments(Bundle args) {  
  2. 660         if (mIndex >= 0) {  
  3. 661             throw new IllegalStateException("Fragment already active");  
  4. 662         }  
  5. 663         mArguments = args;  
  6. 664     }  
  7. 665   
Fragment がアクティブになっている( = mIndex が 0 より大きい)ときに呼ぶと IllegalStateException が投げられることがわかります。

では、mIndex (初期値は -1)はいつセットされるのかというと、FragmentManager の makeActive() メソッドで行われます。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/FragmentManager.java#1009
  1. 1009     void makeActive(Fragment f) {  
  2. 1010         if (f.mIndex >= 0) {  
  3. 1011             return;  
  4. 1012         }  
  5. 1013   
  6. 1014         if (mAvailIndices == null || mAvailIndices.size() <= 0) {  
  7. 1015             if (mActive == null) {  
  8. 1016                 mActive = new ArrayList<fragment>();  
  9. 1017             }  
  10. 1018             f.setIndex(mActive.size());  
  11. 1019             mActive.add(f);  
  12. 1020   
  13. 1021         } else {  
  14. 1022             f.setIndex(mAvailIndices.remove(mAvailIndices.size()-1));  
  15. 1023             mActive.set(f.mIndex, f);  
  16. 1024         }  
  17. 1025     }  
  18. ragment>  


http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/Fragment.java#593
  1. 593     final void setIndex(int index) {  
  2. 594         mIndex = index;  
  3. 595         mWho = "android:fragment:" + mIndex;  
  4. 596    }  
この makeActive() メソッドは FragmentManager の addFragment() から呼ばれています。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/FragmentManager.java#1042
  1.    1042     public void addFragment(Fragment fragment, boolean moveToStateNow) {  
  2.    1043         if (mAdded == null) {  
  3.    1044             mAdded = new ArrayList<fragment>();  
  4.    1045         }  
  5.    1046         if (DEBUG) Log.v(TAG, "add: " + fragment);  
  6.    1047         makeActive(fragment);  
  7.    1048         if (!fragment.mDetached) {  
  8.    1049             mAdded.add(fragment);  
  9.    1050             fragment.mAdded = true;  
  10.    1051             fragment.mRemoving = false;  
  11.    1052             if (fragment.mHasMenu && fragment.mMenuVisible) {  
  12.    1053                 mNeedMenuInvalidate = true;  
  13.    1054             }  
  14.    1055             if (moveToStateNow) {  
  15.    1056                 moveToState(fragment);  
  16.    1057             }  
  17.    1058         }  
  18.    1059     }  
  19. </fragment>  
レイアウトで定義された <fragment> は、 Activity の onCreateView() メソッドからこの addFragment() を呼ぶことで生成されます。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/Activity.java#4189
  1.    4199     public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {  
  2.    4200         if (!"fragment".equals(name)) {  
  3.    4201             return onCreateView(name, context, attrs);  
  4.    4202         }  
  5. ...  
  6.    4223         Fragment fragment = id != View.NO_ID ? mFragments.findFragmentById(id) : null;  
  7. ...  
  8.    4234         if (fragment == null) {  
  9.    4235             fragment = Fragment.instantiate(this, fname);  
  10.    4236             fragment.mFromLayout = true;  
  11.    4237             fragment.mFragmentId = id != 0 ? id : containerId;  
  12.    4238             fragment.mContainerId = containerId;  
  13.    4239             fragment.mTag = tag;  
  14.    4240             fragment.mInLayout = true;  
  15.    4241             fragment.mFragmentManager = mFragments;  
  16.    4242             fragment.onInflate(this, attrs, fragment.mSavedFragmentState);  
  17.    4243             mFragments.addFragment(fragment, true);  
  18. ...  
  19.    4263         }  
  20.    4265         if (fragment.mView == null) {  
  21.    4266             throw new IllegalStateException("Fragment " + fname  
  22.    4267                     + " did not create a view.");  
  23.    4268         }  
  24.    4269         if (id != 0) {  
  25.    4270             fragment.mView.setId(id);  
  26.    4271         }  
  27.    4272         if (fragment.mView.getTag() == null) {  
  28.    4273             fragment.mView.setTag(tag);  
  29.    4274         }  
  30.    4275         return fragment.mView;  
  31.    4276     }  
このとき、引数が2つの Fragment.instantiate(Context context, String fname) で Fragment のインスタンスを生成していることに注目してください。このメソッドは第3引数の Bundle を null として instantiate(Context context, String fname, Bundle args) を呼びます。そのため、レイアウトに定義された Fragment は Argument なし(つまり null)で生成されることがわかります。

この onCreateView() は Activity 内での setContentView() をトリガーとして呼ばれます。

レイアウトから生成される Fragment に Argument をセットしないようになっているのは、必要がないからだと思います。そもそも単体で破棄される Fragment はバックスタックにある場合で、 Fragment のもっている View が Activity のレイアウトの一部になっている場合は Activity と一緒に破棄、再生成されます。 それならば setContentView() の後に FragmentManager#getFragmentById() で Fragment を取得して setter なりで値を渡せばいいわけです。

■ レイアウトから生成される Fragment はバックスタックに移動させないのが普通

実は、レイアウトに定義している Fragment に対し、単に FragmentTransaction の remove() を呼んだ場合、Fragment の保持している View のフィールドは null にセットされますが、レイアウトから View は削除されません。

FragmentManager の moveToState() メソッドを見てみましょう。

初期化の段階(#738)では、レイアウトから生成した Fragment(= mFromLayout が true)の場合この段階で onCreateView() から View を生成し、その処理については LayoutInflater にまかせています。 どういうことかというと、この段階で生成された View (mView として保持される)が Activity の onCreateView() での戻り値になるのです。

生成の段階(#781)では、レイアウトから生成していない Fragment であればコンテナ(Fragment の View の追加先の ViewGroup)の ID からコンテナのインスタンスを取得して mContainer として保持し、onCreateView() から生成した View をこのコンテナに追加しています。

破棄の段階(#874)では、Fragment の持つ View とそのコンテナが両方とも null ではない場合にコンテナから View を削除しています。レイアウトから生成した Fragment はコンテナが null のままなので、この if 文のなかには入らず View は削除されません。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/FragmentManager.java#712
  1.     712     void moveToState(Fragment f, int newState, int transit, int transitionStyle) {  
  2. ...  
  3.     722         if (f.mState < newState) {  
  4. ...  
  5.     737             switch (f.mState) {  
  6.     738                 case Fragment.INITIALIZING:  
  7. ...  
  8.     769                     if (f.mFromLayout) {  
  9.     770                         // For fragments that are part of the content view  
  10.     771                         // layout, we need to instantiate the view immediately  
  11.     772                         // and the inflater will take care of adding it.  
  12.     773                         f.mView = f.onCreateView(f.getLayoutInflater(f.mSavedFragmentState),  
  13.     774                                 null, f.mSavedFragmentState);  
  14.     775                         if (f.mView != null) {  
  15.     776                             f.mView.setSaveFromParentEnabled(false);  
  16.     777                             if (f.mHidden) f.mView.setVisibility(View.GONE);  
  17.     778                             f.onViewCreated(f.mView, f.mSavedFragmentState);  
  18.     779                         }  
  19.     780                     }  
  20.     781                 case Fragment.CREATED:  
  21.     782                     if (newState > Fragment.CREATED) {  
  22. ...  
  23.     784                         if (!f.mFromLayout) {  
  24.     785                             ViewGroup container = null;  
  25.     786                             if (f.mContainerId != 0) {  
  26.     787                                 container = (ViewGroup)mActivity.findViewById(f.mContainerId);  
  27. ...  
  28.     793                             }  
  29.     794                             f.mContainer = container;  
  30.     795                             f.mView = f.onCreateView(f.getLayoutInflater(f.mSavedFragmentState),  
  31.     796                                     container, f.mSavedFragmentState);  
  32.     797                             if (f.mView != null) {  
  33.     798                                 f.mView.setSaveFromParentEnabled(false);  
  34.     799                                 if (container != null) {  
  35. ...  
  36.     806                                     container.addView(f.mView);  
  37.     807                                 }  
  38.     808                                 if (f.mHidden) f.mView.setVisibility(View.GONE);  
  39.     809                                 f.onViewCreated(f.mView, f.mSavedFragmentState);  
  40.     810                             }  
  41. ...  
  42.     811                         }  
  43.     812   
  44. ...  
  45.     849             }  
  46.     850         } else if (f.mState > newState) {  
  47.     851             switch (f.mState) {  
  48. ...  
  49.     873                 case Fragment.STOPPED:  
  50.     874                 case Fragment.ACTIVITY_CREATED:  
  51.     875                     if (newState < Fragment.ACTIVITY_CREATED) {  
  52. ...  
  53.     890                         if (f.mView != null && f.mContainer != null) {  
  54. ...  
  55.     918                             f.mContainer.removeView(f.mView);  
  56.     919                         }  
  57.     920                         f.mContainer = null;  
  58.     921                         f.mView = null;  
  59.     922                     }  
  60. ...  
  61.     970             }  
  62.     971         }  
  63.     972   
  64.     973         f.mState = newState;  
  65.     974     }  
  66.     975   
remove() ではなく replace() で別の Fragment に置き換えた場合、Activity を再生成する(画面回転など)と IllegalStateException で落ちます。

例えば
  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.   
  7.     <Button  
  8.         android:id="@+id/button"  
  9.         android:layout_width="fill_parent"  
  10.         android:layout_height="wrap_content"  
  11.         android:text="show dialog" />  
  12.   
  13.     <FrameLayout  
  14.         android:id="@+id/container"  
  15.         android:layout_width="match_parent"  
  16.         android:layout_height="match_parent" >  
  17.   
  18.         <fragment  
  19.             android:id="@+id/fragment"  
  20.             android:layout_width="match_parent"  
  21.             android:layout_height="match_parent"  
  22.             class="yanzm.example.dialogfragmentsample.MainActivity$MyFragment" />  
  23.     </FrameLayout>  
  24.   
  25. </LinearLayout>  
に対してボタンが押されたら R.id.container 内の Fragment を入れ替えるようにします。
  1. public class MainActivity extends Activity {  
  2.   
  3.     @Override  
  4.     public void onCreate(Bundle savedInstanceState) {  
  5.         super.onCreate(savedInstanceState);  
  6.         setContentView(R.layout.main);  
  7.           
  8.         findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {  
  9.               
  10.             @Override  
  11.             public void onClick(View v) {  
  12.                 switchFragment();  
  13.             }  
  14.         });  
  15.     }  
  16.       
  17.     private void switchFragment() {  
  18.         Fragment fragment = new MyFragment2();  
  19.         getFragmentManager().beginTransaction().replace(R.id.container, fragment).commit();  
  20.     }  
  21.           
  22.     public static class MyFragment extends Fragment {  
  23.   
  24.         @Override  
  25.         public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {  
  26.             return inflater.inflate(R.layout.main2, container, false);  
  27.         }  
  28.     }  
  29.       
  30.     public static class MyFragment2 extends Fragment {  
  31.   
  32.         @Override  
  33.         public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {  
  34.             return inflater.inflate(R.layout.main4, container, false);  
  35.         }  
  36.     }      
  37. }  
ボタンを押して MyFragment を MyFragment2 に入れ替えた状態で画面を回転させると落ちます。

Activity の onCreateView() をもう一度みてみましょう。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/Activity.java#4223
  1.    4223         Fragment fragment = id != View.NO_ID ? mFragments.findFragmentById(id) : null;  
  2.    4224         if (fragment == null && tag != null) {  
  3.    4225             fragment = mFragments.findFragmentByTag(tag);  
  4.    4226         }  
  5.    4227         if (fragment == null && containerId != View.NO_ID) {  
  6.    4228             fragment = mFragments.findFragmentById(containerId);  
  7.    4229         }  
  8. ...  
  9.    4234         if (fragment == null) {  
  10.    4235             fragment = Fragment.instantiate(this, fname);  
  11. ...  
  12.    4263         }  
  13.    4264   
  14.    4265         if (fragment.mView == null) {  
  15.    4266             throw new IllegalStateException("Fragment " + fname  
  16.    4267                     + " did not create a view.");  
  17.    4268         }  
fragment のインスタンスを見つける順番として次の段階を踏みます。

1. android:id で指定されているID
2. 1. で見つからなかったら android:tag で指定されているタグ名
3. 2. でも見つからなかったら親 View の ID

ここで思いだして欲しいのが Fragment は id が明示的に指定されていない場合、親の id を自分の id として持つ、ということです。

上記のコードでは
  1. Fragment fragment = new MyFragment2();        getFragmentManager().beginTransaction().replace(R.id.container, fragment).commit();  
によって、MyFragment2 の id には R.id.container が入ることになります。そのため、画面回転時には

3. 2. でも見つからなかったら親 View の ID

の段階で MyFragment2 のインスタンスが見つかってしまうということです。そのため、#4234 の if 文には入らず、Fragment はクラス名から生成されません。そのまま #4265 に行くのですが、MyFragment2 はレイアウトから生成されたわけではないので、この段階ではまだ View は生成されていません。そのため、IllegalStateException が投げられてしまうのです。

この流れについては、上記の

初期化の段階(#738)では、レイアウトから生成した Fragment(= mFromLayout が true)の場合この段階で onCreateView() から View を生成し、その処理については LayoutInflater にまかせています。 どういうことかというと、この段階で生成された View (mView として保持される)が Activity の onCreateView() での戻り値になるのです。


の部分を思い出してください。



結論としては

レイアウトから生成する Fragment は FragmentTransaction に対象にしない。FragmentTransaction で入れ替える Fragment はコードから生成する。

ということですね。







2012年4月16日月曜日

Android Fragment で setArguments() してるサンプルが多いのはなぜ?

Fragment のサンプルでは、setArguments() を使って Bundle を介して値を渡している例を多く見かけます。

  1. HogeFragment f = new HogeFragment();  
  2. Bundle args = new Bundle();  
  3. args.putInt("num", num);  
  4. f.setArguments(args);  
とやるより

  1. HogeFragment f = new HogeFragment(num);  


  1. HogeFragment f = new HogeFragment();  
  2. f.setNum(num);  
とかやった方がいいんじゃない? Arguments 介するのは面倒じゃない?なにがいいの? と思う人も多いのではないでしょうか。

そこで、Arguments がどういいのかを説明したいと思います。


1. Fragment のコンストラクタで引数を渡すのはダメ

Fragment のリファレンス に書いてあるように、

All subclasses of Fragment must include a public empty constructor. The framework will often re-instantiate a fragment class when needed, in particular during state restore, and needs to be able to find this constructor to instantiate it. If the empty constructor is not available, a runtime exception will occur in some cases during state restore.


Fragment を継承したクラスは空のコンストラクタを用意しないと行けません。メモリが足りなくなったときに Activity スタック内の(バックグラウンドの)Activity が破棄されるように、Fragment もメモリが足りなくなったときは破棄されます。破棄された Fragment が再び必要になったときにシステムは空のコンストラクタから Fragment のインスタンスを再生成します。
*そのため、空のインスタンスの用意されていない Fragment が上記の状態に遭遇するとアプリが落ちます。


2. setter では再生成時に値を渡せない

上記の再生成時に使われるのが Fragment#instantiate(Context context, String fname, Bundle args) メソッドです。 このメソッドは引数で渡されたクラス名の Fragment の空のコンストラクタを呼び出してインスタンスを生成し、第3引数でセットした Bundle を setArguments() でセットしてから返します。

つまり、② だと、再生成されたときに引数に num を取るコンストラクタが呼ばれないし、③ だと、再生成されたときに setter が呼ばれないため num を Fragment に渡せません。 しかし、① であれば、HogeFragment が破棄されるときにシステムが getArguments() で num の値を Bundle に保存して再生成のときにその Bundle を setArguemnts() でセットしてくれるので、再生成時も num の値を HogeFragment に渡すことができるのです。


この再生成時に値を渡せないという問題は DialogFragment ではより顕著になります。
例えば、次のようにボタンを押したらダイアログが表示されるというコードがあるとします。
  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.   
  7.     <Button  
  8.         android:id="@+id/button"  
  9.         android:layout_width="fill_parent"  
  10.         android:layout_height="wrap_content"  
  11.         android:text="show dialog" />  
  12.   
  13. </LinearLayout>  
  1. public class MainActivity extends Activity {  
  2.   
  3.     @Override  
  4.     public void onCreate(Bundle savedInstanceState) {  
  5.         super.onCreate(savedInstanceState);  
  6.         setContentView(R.layout.main);  
  7.           
  8.         findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {  
  9.               
  10.             @Override  
  11.             public void onClick(View v) {  
  12.                 showDialog();  
  13.             }  
  14.         });  
  15.     }  
  16.       
  17.       
  18.     void showDialog() {  
  19.         MyAlertDialogFragment newFragment = MyAlertDialogFragment.newInstance(R.string.hello);  
  20.         newFragment.setNum(10);  
  21.         newFragment.show(getFragmentManager(), "dialog");  
  22.     }  
  23.   
  24.     public static class MyAlertDialogFragment extends DialogFragment {  
  25.   
  26.         private int mNum;  
  27.   
  28.         public static MyAlertDialogFragment newInstance(int title) {  
  29.             MyAlertDialogFragment frag = new MyAlertDialogFragment();  
  30.             Bundle args = new Bundle();  
  31.             args.putInt("title", title);  
  32.             frag.setArguments(args);  
  33.             return frag;  
  34.         }  
  35.   
  36.         public void setNum(int num) {  
  37.             mNum = num;  
  38.         }  
  39.   
  40.         @Override  
  41.         public Dialog onCreateDialog(Bundle savedInstanceState) {  
  42.             int title = getArguments().getInt("title");  
  43.   
  44.             return new AlertDialog.Builder(getActivity())  
  45.                 .setIcon(android.R.drawable.ic_dialog_alert)  
  46.                 .setTitle(title)  
  47.                 .setMessage(mNum + "")  
  48.                 .setPositiveButton(android.R.string.ok, null)  
  49.                 .setNegativeButton(android.R.string.cancel, null)  
  50.                 .create();  
  51.         }  
  52.     }  
  53. }  
DialogFragment を使うと、ダイアログが表示されている状態で Activity が再生成された場合に自動でダイアログも再表示してくれます。

例えば、上記のコードでボタンを押してダイアログが表示された状態で画面を回転させると新しく onCreate() が呼ばれますが、ボタンは押されてないので本来はダイアログも消えてしまいます。しかし Dialog ではなく DialogFragment を使っているのでシステムが DialogFragment を再生成して再び表示してくれるのです。

この際の再生成は上記コードの showDialog() を通らず、空のコンストラクタから生成されるため setNum() は呼ばれません。よって回転させると、Arguments としてセットしたタイトルは回転前と同じですが、メッセージ(mNum の値)は 10 ではなく 0 になります。


このように、Fragment はシステムから再生成されることが多いので、setter を使うよりも Arguments を介したほうが最終的にいろいろ楽になります。
ということで、オレオレ setter ではなく Arguemts を使うようにしましょう!



2012年4月12日木曜日

Android ListView でデータが空のときもヘッダー・フッターを表示する

ListView には addHeaderView()addFooterView() でヘッダーやフッターをつけることができます。

また、ListView にはリストのデータが空の時に表示させる emptyView を指定することができます。データがないときに画面が真っ黒になるとユーザーはアプリが壊れたと思ってしまうかもしれないので、空のときにはメッセージをだしましょうとよく言われます。

ただ、この emptyView を指定するとリストのデータが空のときに、ヘッダーやフッターも表示されなくなります。

emptyView を指定している状態でヘッダーやフッターを表示できるのか調べてみました。

まず、ListView で emptyView への切り替えをどこでしているかいうと AdapterView の updateEmptyStatus(boolean empty) です。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/widget/AdapterView.java#717
  1. 717     private void updateEmptyStatus(boolean empty) {  
  2. 718         if (isInFilterMode()) {  
  3. 719             empty = false;  
  4. 720         }  
  5. 721   
  6. 722         if (empty) {  
  7. 723             if (mEmptyView != null) {  
  8. 724                 mEmptyView.setVisibility(View.VISIBLE);  
  9. 725                 setVisibility(View.GONE);  
  10. 726             } else {  
  11. 728                 setVisibility(View.VISIBLE);  
  12. 729             }  
  13. 730   
  14. 734             if (mDataChanged) {  
  15. 735                 this.onLayout(false, mLeft, mTop, mRight, mBottom);  
  16. 736             }  
  17. 737         } else {  
  18. 738             if (mEmptyView != null) mEmptyView.setVisibility(View.GONE);  
  19. 739             setVisibility(View.VISIBLE);  
  20. 740         }  
  21. 741     }  
ListView には setFilterText() でフィルターをセットできるのですが、このフィルターモードの場合はリストに表示するデータがなくても、フィルターに一致するデータがないというだけで実際のデータが空というわけではないので、最初の if 文で false にしています。

その次の if else 文が本体と emptyView の表示・非表示の切り替えをしているところです。
これをみると empty が true でも emptyView があれば本体が表示されることがわかります。

では、この updateEmptyStatus() がどこから呼ばれているかというと

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/widget/AdapterView.java#643
setEmptyView()
  1. 643     public void setEmptyView(View emptyView) {  
  2. 644         mEmptyView = emptyView;  
  3. 645   
  4. 646         final T adapter = getAdapter();  
  5. 647         final boolean empty = ((adapter == null) || adapter.isEmpty());  
  6. 648         updateEmptyStatus(empty);  
  7. 649     }  


http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/widget/AdapterView.java#717
checkFocus()
  1. 698     void checkFocus() {  
  2. 699         final T adapter = getAdapter();  
  3. 700         final boolean empty = adapter == null || adapter.getCount() == 0;  
  4. 701         final boolean focusable = !empty || isInFilterMode();  
  5.   
  6. 705         super.setFocusableInTouchMode(focusable && mDesiredFocusableInTouchModeState);  
  7. 706         super.setFocusable(focusable && mDesiredFocusableState);  
  8. 707         if (mEmptyView != null) {  
  9. 708             updateEmptyStatus((adapter == null) || adapter.isEmpty());  
  10. 709         }  
  11. 710     }  
の2カ所です。

いずれも adapter が null もしくは adapter.isEmpty() が true なら updateEmptyStatus() の引数として true が渡されています。

つまりまとめると

1. adapter != null && adapter.isEmpty == false → 本体が表示される
2. adapter == null or adapter.isEmpty == true
  → emptyView != null → 本体 が表示される
  → emptyView == null → emptyView が表示される

なので、結論としては、 isEmpty() で常に false を返すように Override するか、emptyView を null にすればよい。

ListView を単体で使うときは明示的に setEmptyView() するか、android/id:empty の View を XML で定義するので意識できるますが、Android で用意されている ListView 用の ListActivity と ListFragment を使うときにはちょっと注意が必要です。

Android 4.0 では、ListActivity でのデフォルトのレイアウトとして com.android.internal.R.layout.list_content_simple をセットしています。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/ListActivity.java#308
  1. 308     private void ensureList() {  
  2. 309         if (mList != null) {  
  3. 310             return;  
  4. 311         }  
  5. 312         setContentView(com.android.internal.R.layout.list_content_simple);  
  6. 313   
  7. 314     }  

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/res/res/layout/list_content_simple.xml
  1. 20 <listview xmlns:android="http://schemas.android.com/apk/res/android" android:id="@android:id/list" 21="" android:layout_width="match_parent" 22="" android:layout_height="match_parent" 23="" android:drawselectorontop="false" 24="">  
  2. tview>  

ちなみに Android 2.3.4 では、com.android.internal.R.layout.list_content をセットしていますが、なかのレイアウトは 4.0 の list_content_simple と同じです。

http://tools.oesf.biz/android-2.3.4_r1.0/xref/frameworks/base/core/java/android/app/ListActivity.java#308
  1. 308     private void ensureList() {  
  2. 309         if (mList != null) {  
  3. 310             return;  
  4. 311         }  
  5. 312         setContentView(com.android.internal.R.layout.list_content);  
  6. 313   
  7. 314     }  

http://tools.oesf.biz/android-2.3.4_r1.0/xref/frameworks/base/core/res/res/layout/list_content.xml
  1. 20 <ListView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@android:id/list"  
  2. 21     android:layout_width="match_parent"   
  3. 22     android:layout_height="match_parent"  
  4. 23     android:drawSelectorOnTop="false"  
  5. 24     />  

一方、ListFragment のデフォルトのレイアウトとしては com.android.internal.R.layout.list_content がセットされています。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/ListFragment.java#191
  1. 190     @Override  
  2. 191     public View onCreateView(LayoutInflater inflater, ViewGroup container,  
  3. 192             Bundle savedInstanceState) {  
  4. 193         return inflater.inflate(com.android.internal.R.layout.list_content,  
  5. 194                 container, false);  
  6. 195     }  

こっちはちょっと複雑なレイアウトになっています。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/res/res/layout/list_content.xml
  1. 18 <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2. 19         android:layout_width="match_parent"  
  3. 20         android:layout_height="match_parent">  
  4. 21       
  5. 22     <LinearLayout android:id="@+id/progressContainer"  
  6. 23             android:orientation="vertical"  
  7. 24             android:layout_width="match_parent"   
  8. 25             android:layout_height="match_parent"  
  9. 26             android:visibility="gone"  
  10. 27             android:gravity="center">  
  11. 28           
  12. 29         <ProgressBar style="?android:attr/progressBarStyleLarge"  
  13. 30                 android:layout_width="wrap_content"  
  14. 31                 android:layout_height="wrap_content" />  
  15. 32         <TextView android:layout_width="wrap_content"  
  16. 33                 android:layout_height="wrap_content"  
  17. 34                 android:textAppearance="?android:attr/textAppearanceSmall"  
  18. 35                 android:text="@string/loading"  
  19. 36                 android:paddingTop="4dip"  
  20. 37                 android:singleLine="true" />  
  21. 38               
  22. 39     </LinearLayout>  
  23. 40           
  24. 41     <FrameLayout android:id="@+id/listContainer"  
  25. 42             android:layout_width="match_parent"   
  26. 43             android:layout_height="match_parent">  
  27. 44               
  28. 45         <ListView android:id="@android:id/list"  
  29. 46                 android:layout_width="match_parent"   
  30. 47                 android:layout_height="match_parent"  
  31. 48                 android:drawSelectorOnTop="false" />  
  32. 49         <TextView android:id="@+android:id/internalEmpty"  
  33. 50                 android:layout_width="match_parent"  
  34. 51                 android:layout_height="match_parent"  
  35. 52                 android:gravity="center"  
  36. 53                 android:textAppearance="?android:attr/textAppearanceLarge" />  
  37. 54     </FrameLayout>  
  38. 55           
  39. 56 </FrameLayout>  


注目してほしいのが @+android:id/internalEmpty という ID の TextView です。
ListFragment には setEmptyText() という、データが空のときに表示する文字をセットするメソッドが用意されています。このメソッドが呼ばれると、次のように ListView の setEmptyView() が呼ばれます。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/ListFragment.java#289
  1. 289     public void setEmptyText(CharSequence text) {  
  2. 290         ensureList();  
  3. 291         if (mStandardEmptyView == null) {  
  4. 292             throw new IllegalStateException("Can't be used with a custom content view");  
  5. 293         }  
  6. 294         mStandardEmptyView.setText(text);  
  7. 295         if (mEmptyText == null) {  
  8. 296             mList.setEmptyView(mStandardEmptyView);  
  9. 297         }  
  10. 298         mEmptyText = text;  
  11. 299     }  

ここの mStandardEmptyView というのが上記の @+android:id/internalEmpty という ID の TextView に対応しています。


ということで、ListFragment でなんとなくやってた setEmptyText() をコメントアウトしたらヘッダーでるようになったー!

ただし、残念ながらこの場合も

-----
ヘッダー
empty message
フッター
-----

のようにはできないです。ヘッダー・フッターと emptyView は一緒に出すことはコードを見た限りではできないですねー

updateEmptyStatus が protected だったらいろいろできたのに。。。

やるとしたらこんな感じかな。
ヘッダーと、emptyView を同じレイアウトXMLから生成するくらいしか方法がないかな。

  1. public class MainActivity extends ListActivity implements View.OnClickListener{  
  2.   
  3.     ArrayAdapter<String> mAdapter;  
  4.       
  5.     @Override  
  6.     public void onCreate(Bundle savedInstanceState) {  
  7.         super.onCreate(savedInstanceState);  
  8.         setContentView(R.layout.main);  
  9.   
  10.         mAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1);  
  11.           
  12.         LayoutInflater inflater = getLayoutInflater();  
  13.         View header = inflater.inflate(R.layout.header, nullfalse);  
  14.   
  15.         getListView().addHeaderView(header);  
  16.         setListAdapter(mAdapter);  
  17.           
  18.         View emptyHeader = getListView().getEmptyView();  
  19.           
  20.         emptyHeader.setOnClickListener(this);  
  21.         header.setOnClickListener(this);  
  22.     }  
  23.   
  24.     @Override  
  25.     public void onClick(View v) {  
  26.         if(mAdapter.isEmpty()) {  
  27.             mAdapter.add("Test");  
  28.         }  
  29.         else {  
  30.             mAdapter.remove("Test");  
  31.         }  
  32.     }  
  33. }  
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  3.     android:layout_width="match_parent"  
  4.     android:layout_height="match_parent" >  
  5.   
  6.     <ListView  
  7.         android:id="@android:id/list"  
  8.         android:layout_width="match_parent"  
  9.         android:layout_height="match_parent"  
  10.         android:drawSelectorOnTop="false" />  
  11.   
  12.     <LinearLayout  
  13.         android:id="@android:id/empty"  
  14.         android:layout_width="match_parent"  
  15.         android:layout_height="match_parent"  
  16.         android:orientation="vertical" >  
  17.   
  18.         <include layout="@layout/header"/>  
  19.   
  20.         <TextView  
  21.             android:layout_width="match_parent"  
  22.             android:layout_height="0dip"  
  23.             android:layout_weight="1"  
  24.             android:gravity="center"  
  25.             android:text="No data"  
  26.             android:textSize="30sp" />  
  27.     </LinearLayout>  
  28.   
  29. </FrameLayout>  
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <Button xmlns:android="http://schemas.android.com/apk/res/android"  
  3.     android:id="@+id/header"  
  4.     android:layout_width="match_parent"  
  5.     android:layout_height="wrap_content"  
  6.     android:text="Header" />  
ListFragment でも同じ感じ。

  1. public class MainFragment extends ListFragment implements View.OnClickListener {  
  2.   
  3.   
  4.     ArrayAdapter<String> mAdapter;  
  5.       
  6.     @Override  
  7.     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {  
  8.         return inflater.inflate(R.layout.main, container, false);  
  9.     }  
  10.   
  11.     @Override  
  12.     public void onActivityCreated(Bundle savedInstanceState) {  
  13.         super.onActivityCreated(savedInstanceState);  
  14.   
  15.         mAdapter = new ArrayAdapter<String>(getActivity(), android.R.layout.simple_list_item_1);  
  16.           
  17.         LayoutInflater inflater = getActivity().getLayoutInflater();  
  18.         View header = inflater.inflate(R.layout.header, nullfalse);  
  19.   
  20.         getListView().addHeaderView(header);  
  21.         setListAdapter(mAdapter);  
  22.           
  23.         View emptyHeader = getListView().getEmptyView();  
  24.           
  25.         emptyHeader.setOnClickListener(this);  
  26.         header.setOnClickListener(this);  
  27.     }  
  28.   
  29.     @Override  
  30.     public void onClick(View v) {  
  31.         if(mAdapter.isEmpty()) {  
  32.             mAdapter.add("Test");  
  33.         }  
  34.         else {  
  35.             mAdapter.remove("Test");  
  36.         }  
  37.     }  
  38. }  


もちろん Adapter を extends して isEmpty() で true を返すようにすれば emptyView がセットされていても本体が表示されるようになります。 具体的な用途は思いつかないですが、データのあるなしにかかわらず独自の基準で emptyView の表示・非表示を切り替えたい場合には便利だと思います。


ちなみに、ListFragment で emptyView を使う場合ははまりポイントがいっぱいなので、 このエントリを見ておくことをオススメします!
Y.A.M の 雑記帳: Android ListFragment でカスタムレイアウトを使うと setEmptyText() が使えない -





2012年4月10日火曜日

DartEditor で zen-coding を使う

DartEditor は Eclipse ベースのエディターなので、試しに zen-coding の Eclipse のプラグインを移したら、できるようになりました!わーい。

zen-coding のサイト
zen-coding - Set of plugins for HTML and CSS hi-speed coding - Google Project Hosting -

から、Eclipse 用の plugin-in のページ
sergeche/eclipse-zencoding - http://goo.gl/3n7aZ

にいくと、Eclipse へのインストール方法が書いてあります。
簡単にいうと、[Help] - [Install New Software...] で http://zen-coding.ru/eclipse/updates/ (もしくは http://media.chikuyonok.ru/eclipse/updates/) を指定してインストールします。

このプラグインがインストールされた Eclipse のディレクトリの中の
  • features/ru.zencoding.eclipse_xxx
  • plugins/ru.zencoding.eclipse_xxx
をそれぞれ DartEditor の features/ と plugins/ にコピーします。

さらに、configuration/org.eclipse.equinox.simpleconfigurator/bundles.info の中の

ru.zencoding.eclipse,0.8.0.201201232221,plugins/ru.zencoding.eclipse_0.8.0.201201232221.jar,4,false

を DartEditor の configuration/org.eclipse.equinox.simpleconfigurator/bundles.info の中に追加します。


ここまでやったら後は DartEditor を起動するだけ。

DartEditor の Prefereces に Zen Coding の項目が出ていれば成功!



html ファイルで html:5 と打ってタブを押したら↓に変換されるー!
  1. <!DOCTYPE HTML>  
  2. <html lang="en-US">  
  3. <head>  
  4.  <meta charset="UTF-8">  
  5.  <title></title>  
  6. </head>  
  7. <body>  
  8.    
  9. </body>  
  10. </html>  






DartEditor で less を使う

less の javascript ファイルを読み込んで動的に css に変換する場合、その script タグが書かれている html ファイルに file:///... でアクセスすると Chromium や Chrome ではうまくうごきません。

css - less.js not working in chrome - Stack Overflow -

これは Chrome のセッキュリティ機能の1つで、ローカルの javascript ファイル読み込むときに起こる問題として知られているものです。

--allow-file-access-from-files フラグをつけて Chromium や Chrome を起動すれば less の javascript ファイルが正しく動きます。
List of Chromium Command Line Switches

Dart のアプリケーションで less を使いたい場合、デフォルトで Dartium (Chromium をベースにしている) が起動するので、上記と同じ問題が起こります。

そこで、Dartium を起動するときに --allow-file-access-from-files フラグをつけられればいいということになります。

Dart アプリケーションの起動は、DartEditor の [Tools] - [Manage Launches...] でカスタマイズできます。
  • Dartium Launch ではフラグも設定できないし、起動するブラウザも指定できません(=ショートカットとか作れない)
  • Dart Web Launch ではフラグは設定できませんが、起動するブラウザを指定できます。さらに指定されたブラウザがすでに起動している場合は、新しいタブで実行されます。


Dartium Launch


Dart Web Launch


そのため、
  • 1. DartEditor の起動設定で dark-sdk 内の Chromium を起動する Dart Web Launch を作っておく(起動ターゲットは less を使った Dart アプリケーション)
  • 2. コンソールから --allow-file-access-from-files をつけて dart-sdk 内の Chromium を起動する
  • 3. DartEditor から less を使った Dart アプリケーションを 1. の Dart Web Launch で起動する

これで DartEditor で作成しているローカルの Dart アプリケーションで less が動くようになります。

*アプリ毎に Dart Web Launch 作るのは面倒なので、デフォルトの Dartium に flag 指定できるようにならないかなー。。。



2012年4月5日木曜日

Dart Canvas を使う

dart で Canvas を使うには、document.query() で canvas タグのエレメントを CanvasElement として取得し、その CanvasElement に対して getContext() を呼んで CanvasRenderingContext2D を取得します。

あとは、CanvasRenderingContext2D オブジェクトに対して fillRect() などの Canvas のメソッドを呼べば OK

  1. #import('dart:html');  
  2.   
  3. class Droid {  
  4.   
  5.   CanvasRenderingContext2D ctx;  
  6.   
  7.   static final PI = Math.PI;  
  8.   static final String ORANGE = "orange";  
  9.   static final String WHITE = "white";  
  10.     
  11.   static final int gap = 5;  
  12.   static final int bodyWidth = 150;  
  13.   static final int bodyHeight = 120;  
  14.   static final int armWidth = 30;  
  15.   static final int armHeight = 70;  
  16.   static final int legWidth = 30;  
  17.   static final int legHeight = 35;  
  18.     
  19.   Droid() {  
  20.     CanvasElement canvas = document.query("#canvas");  
  21.     ctx = canvas.getContext("2d");  
  22.       
  23.     drawFrame();  
  24.   }  
  25.   
  26.   // Draw the complete figure for the current number of seeds.  
  27.   void drawFrame() {  
  28.     ctx.clearRect(0, 0, 300, 300);  
  29.       
  30.     ctx.lineWidth = 2;  
  31.     ctx.fillStyle = ORANGE;  
  32.     ctx.strokeStyle = ORANGE;  
  33.   
  34.     ctx.save();  
  35.       
  36.     // head  
  37.     ctx.beginPath();  
  38.     ctx.translate(0, bodyWidth / 2 * 0.13);  
  39.     ctx.scale(1, 0.9);  
  40.     ctx.arc(100 + bodyWidth / 2, 100 - gap, bodyWidth / 2, PI, 0, false);  
  41.     ctx.fill();  
  42.     ctx.closePath();  
  43.   
  44.     ctx.restore();  
  45.       
  46.     // body  
  47.     ctx.beginPath();  
  48.     ctx.moveTo(100, 100);  
  49.     ctx.lineTo(100 + bodyWidth, 100);  
  50.     ctx.arc(100 + bodyWidth - 15, 100 + bodyHeight - 15, 15, 0, PI / 2, false);  
  51.     ctx.arc(100 + 15, 100 + bodyHeight - 15, 15, PI / 2, PI, false);  
  52.     ctx.fill();  
  53.     ctx.closePath();  
  54.   
  55.       
  56.     // legs left  
  57.     drawLeg(100 + bodyWidth / 2 - 15 - legWidth, 100 + bodyHeight);  
  58.   
  59.     // legs rihgt  
  60.     drawLeg(100 + bodyWidth / 2 + 15, 100 + bodyHeight);  
  61.   
  62.       
  63.     // arms left  
  64.     drawArm(100 - gap - armWidth, 100 + armWidth / 2);  
  65.   
  66.     // arms right  
  67.     drawArm(100 + bodyWidth + gap, 100 + armWidth / 2);      
  68.   
  69.     // nidle right  
  70.     ctx.lineWidth = 5;  
  71.     ctx.lineCap = "round";  
  72.       
  73.     ctx.beginPath();  
  74.     ctx.moveTo(100 + bodyWidth / 2 - 45, 12);  
  75.     ctx.lineTo(100 + bodyWidth / 2 - 20, 60);  
  76.     ctx.closePath();  
  77.     ctx.stroke();  
  78.   
  79.     // nidle left  
  80.     ctx.beginPath();  
  81.     ctx.moveTo(100 + bodyWidth / 2 + 45, 12);  
  82.     ctx.lineTo(100 + bodyWidth / 2 + 20, 60);  
  83.     ctx.closePath();  
  84.     ctx.stroke();  
  85.   
  86.       
  87.     // eye  
  88.     ctx.fillStyle = WHITE;  
  89.   
  90.     ctx.beginPath();  
  91.     ctx.arc(100 + bodyWidth / 2 - 34, 65, 6, 0, 2 * PI, false);  
  92.     ctx.fill();  
  93.     ctx.closePath();  
  94.   
  95.     ctx.beginPath();  
  96.     ctx.arc(100 + bodyWidth / 2 + 34, 65, 6, 0, 2 * PI, false);  
  97.     ctx.fill();  
  98.     ctx.closePath();  
  99.   }  
  100.     
  101.   void drawArm(int left, int top) {  
  102.     ctx.beginPath();  
  103.     ctx.arc(left + armWidth / 2, top, armWidth / 2, PI, 0, false);  
  104.     ctx.lineTo(left + armWidth, top + armHeight);  
  105.     ctx.arc(left + armWidth / 2, top + armHeight, armWidth / 2, 0, PI, false);  
  106.     ctx.lineTo(left, top);  
  107.     ctx.fill();  
  108.     ctx.closePath();  
  109.   }  
  110.     
  111.   void drawLeg(int left, int top) {  
  112.     ctx.beginPath();  
  113.     ctx.moveTo(left, top);  
  114.     ctx.lineTo(left, top + legHeight);  
  115.     ctx.arc(left + legWidth / 2, top + legHeight, legWidth / 2, PI, 0, true);  
  116.     ctx.lineTo(left + legWidth, top);  
  117.     ctx.fill();  
  118.     ctx.closePath();  
  119.   }  
  120. }  
  121.   
  122. void main() {  
  123.   new Droid();  
  124. }  


  1. <!DOCTYPE html>  
  2.   
  3. <html>  
  4.   <head>  
  5.     <title>Droid</title>  
  6.   </head>  
  7.   <body>  
  8.     <h1>Droid</h1>  
  9.       
  10.     <div>  
  11.         <canvas id="canvas" width="300" height="300"></canvas>  
  12.     </div>  
  13.       
  14.     <script type="application/dart" src="MoveDroid.dart"></script>  
  15.     <script src="http://dart.googlecode.com/svn/branches/bleeding_edge/dart/client/dart.js"></script>  
  16.   </body>  
  17. </html>  








2012年4月4日水曜日

Dart DOMを操作する

  1. #import('dart:html');  
  2.   
  3. void main() {  
  4.   document.query("#status").innerHTML = "Hello World!";  
  5. }  
Dart で DOM 操作をするには html ライブラリを使います。
SVG や WebGL のインタフェースなども用意されています。

Library html

Document クラスのオブジェクトを取得する getter が定義されていて、

html_dartium.dat
  1. #library('html');  
  2. ...  
  3. Document get document() {  
  4.   if (__document == null) {  
  5.     _initialize();  
  6.   }  
  7.   return __document;  
  8. }  
main() のなかの document はこの getter を呼んでいる = Document のオブジェクトということです。

id でエレメントを見つける
  1. document.query('#selector');  

class で最初のエレメントを1つ見つける
  1. document.query('.classname');  

class で複数のエレメントを見つける
  1. document.queryAll('.classname');  

tag で最初のエレメントを1つ見つける
  1. document.query('div');  

tag で複数のエレメントを見つける
  1. document.queryAll('div');  

name で最初のエレメントを1つ見つける
  1. document.query('[name="form"]');  

name で複数のエレメントを見つける
  1. document.queryAll('[name="form"]');  


最初の child node を取得する
  1. element.nodes[0];  

最初の child element を取得する
  1. element.elements[0];  


エレメントを tag から作成
  1. Element element = new Element.tag('div');  

エレメントを html から作成
  1. Element element = new Element.html('<p>html string</p>');  
* html の引数の html 文字が正しい文法になっていないといけない。


エレメントに子エレメントを追加
  1. element.elements.add(newElement);  


エレメントにイベントハンドラを追加
  1. element.on.click.add(handleClick);  

エレメントからイベントハンドラを削除
  1. element.on.click.remove(handleClick);  
* element.on.hoge.add(handleEvent); / element.on.hoge.remove(handleEvent);
element.on は ElementEvents で例えば
  • click
  • doubleClick
  • drag
  • drop
  • focus
  • keyDown
  • mouseDown ...
などたくさんある。

  1. #import('dart:html');  
  2.   
  3. void main() {  
  4.   
  5.   // id でエレメント取得  
  6.   document.query("#status").innerHTML = "Find one element by id";  
  7.     
  8.   // class で最初のエレメント取得  
  9.   document.query(".message").innerHTML = "Find one element by class";  
  10.   
  11.   // class で複数のエレメント取得  
  12.   ElementList elements = document.queryAll(".message2");  
  13.   for(Element e in elements) {  
  14.     e.innerHTML = "Find many elements by class";  
  15.   }  
  16.     
  17.   // tag で最初のエレメント取得  
  18.   document.query('h2').style.background = "#9999ff";  
  19.     
  20.   // tag で複数のエレメント取得  
  21.   document.queryAll('div').forEach((Element e) {  
  22.     e.style.background = "#ccccff";  
  23.   });  
  24.     
  25.   // name で最初のエレメント取得  
  26.   document.query('[name="form"]').style.background = "#ffeeee";  
  27.     
  28.   // name で複数のエレメント取得  
  29.   document.queryAll('[name="form"]');  
  30.     
  31.   // child nodeを取得  
  32.   Element elem = document.query("#main");  
  33.   if(!elem.nodes.isEmpty()) {  
  34.     Node node = elem.nodes[0];  
  35.   }  
  36.   
  37.   // element を tag から作成  
  38.   Element element = new Element.tag('div');  
  39.   element.innerHTML = "create new an element from tag";  
  40.     
  41.   // element を html から作成  
  42.   Element element2 = new Element.html('<p>create new an element from html</p>');  
  43.     
  44.   // element を追加  
  45.   elem.elements.add(element);  
  46.   elem.elements.add(element2);  
  47.     
  48.   // element を削除  
  49.   elem.elements[0].remove();  
  50.     
  51.   // イベントハンドラ  
  52.   Element btn = document.query("#button");  
  53.   btn.on.click.add((e) {  
  54.     btn.innerHTML = "Clicked!";  
  55.   });  
  56. }  
  1. <!DOCTYPE html>  
  2.   
  3. <html>  
  4.   <head>  
  5.     <title>hellodart</title>  
  6.   </head>  
  7.   <body>  
  8.     <h2 id="status">dart is not running</h2>  
  9.       
  10.     <div class="message"></div>  
  11.     <div class="message2"></div>  
  12.     <div class="message2"></div>  
  13.       
  14.     <input name="form"></input>  
  15.       
  16.     <div id="main">  
  17.       <div>First</div>  
  18.     </div>  
  19.       
  20.     <div id="button">Click here!</div>  
  21.   
  22.   
  23.     <script type="application/dart" src="hellodart.dart"></script>  
  24.     <script src="http://dart.googlecode.com/svn/branches/bleeding_edge/dart/client/dart.js"></script>  
  25.   </body>  
  26. </html>  

2012年4月3日火曜日

Hello Dart 2

変数の定義
  • var hoge;
  • final hoge;
_ で始まる変数は private になる。それ以外は public になる。
初期化されていない変数は初期値は null
  1. main() {  
  2.   final name = "droid";  
  3.   var age = 4;  
  4.   var favor = 'ics';  
  5.   
  6.   // コンソールに出力  
  7.   print("Hello, my name is ${name}.");  
  8.   print('I like ' + favor + '!');  
  9. }  
文字列に変数を埋め込む方法
  • "Hello, ${name}"
  • 'Hello, $name'
  • 'Hello' + name
複数行の文字列は '''...''' もしくは """...""" で表現できる
@"..." で /n などが無視される raw string になる


■ クラス

class Hoge {}
  1. class Droid {  
  2.   var name = 'droid';  
  3.   
  4.   greet(favor) {  
  5.     print("Hello, my name is ${name}.");  
  6.     print('I like ' + favor + '!');  
  7.   }  
  8. }  
  9.   
  10. main () {  
  11.   var droid = new Droid();  
  12.   droid.greet('ics');  
  13. }  

dart では全てのクラスは Object クラスを継承している non-Object クラスを継承したい場合は extends を使う

通常の変数と同じように、生成したクラスのインスタンス変数には var か final もしくは各タイプを使う必要がある

コンストラクタを明示的に定義していない場合は、引数なしのコンストラクタが定義されていることになる
コンストラクタを明示的に定義した場合は、引数なしのコンストラクタも必要な場合は明示的に定義する必要がある
  1. class Droid {  
  2.   var name = 'droid';  
  3.   
  4.   // コンストラクタ  
  5.   Droid();  
  6.   Droid.withName(this.name);  
  7.   
  8.   greet(favor) {  
  9.     print("Hello, my name is ${name}.");  
  10.     print('I like ' + favor + '!');  
  11.   }  
  12. }  
  13.   
  14. main () {  
  15.   var droid = new Droid.withName('dronjo');  
  16.   droid.greet('gingerbread');  
  17. }  
getter と setter

_ で始まる変数は private になる。それ以外は public になる。
  1. class Droid {  
  2.   String _name = 'droid';  // private  
  3.   
  4.   String get name() => _name;  // getter  
  5.   
  6.   void set name(String name) {  // setter  
  7.     if(name == null) name = "";  
  8.     if(name.length > 20) throw 'name is too long!';  
  9.     _name = name;  
  10.   }  
  11.   
  12.   greet(favor) {  
  13.     print("Hello, my name is ${name}.");  
  14.     print('I like ' + favor + '!');  
  15.   }  
  16. }  
  17.   
  18. main () {  
  19.   var droid = new Droid();  
  20.   droid.name = 'dronjo';  
  21.   droid.greet('gingerbread');  
  22. }  


■ Interfaces

implements Hoge
  1. class Droid implements Comparable {  
  2.   String name = 'droid';  // private  
  3.   
  4.   // コンストラクタ  
  5.   Droid();  
  6.   Droid.withName(this.name);  
  7.   
  8.   greet(String text) => print('$name, $text');  
  9.     
  10.   int compareTo(Droid d) => name.compareTo(d.name);  
  11. }  
  12.   
  13. main () {  
  14.   var droid = new Droid();  
  15.   droid.name = 'dronjo';  
  16.   
  17.   var droid2 = new Droid.withName('dronjo');  
  18.   droid.greet('gingerbread');  
  19.   
  20.   num result = droid2.compareTo(droid);  
  21.   if(result == 0) {  
  22.     droid2.greet('you are the same.');  
  23.   } else {  
  24.     droid2.greet('you are different.');  
  25.   }  
  26. }  
int は primitive ではなく、interface!

Optional types (実態は interfaces)
String, num, int (num を継承), double (num を継承), 等
自分で static な type を追加することもできる
final を加えて定義できる final String hoge;


Built-in types
  • String
  • StringBuffer
  • num
  • int (num を継承)
  • double (num を継承)
  • bool
  • List (JavaScript の array にコンパイルされる)
  • Map
String <--> in, double 変換
  1. // string -> int  
  2. var one = Math.parseInt("1");                   // 1  
  3.   
  4. // string -> double  
  5. var onePointOne = Math.parseDouble("1.1");      // 1.1  
  6.   
  7. // int -> string  
  8. String oneAsString = 1.toString();              // "1"  
  9.   
  10. // double -> string  
  11. String piAsString = 3.14159.toStringAsFixed(2); // "3.14"  
JavaScript と違って、bool は 1 や non-null オブジェクトを true としては扱わない



■ List
  1. // 可変長リスト  
  2. var list = [1,2,3];  
  3.   
  4. // リストの長さ  
  5. print(list.length);  
  6.   
  7. // 要素の取得  
  8. print(list[1]);  
  9.   
  10. // 要素の追加  
  11. list.add(4);  
  12.   
  13. // 要素の削除  
  14. list.removeRange(2, 1);  
  1. // 固定長リスト  
  2. var list = new List(4);  
Iterating

for...in
  1. var list = [1,2,3];  
  2. for (final x in list) {  
  3.   print(x);  
  4. }  

forEach()
  1. var list = [1,2,3];  
  2. void printElement(element) => print(element);  
  3. list.forEach(printElement);  
  1. var list = [1,2,3];  
  2. list.forEach((element) => print(element));  
List document
Collection document


■ Map
  1. var gifts = {  
  2. // keys       values  
  3.   "first"  : "partridge",  
  4.   "second" : "turtledoves",  
  5.   "fifth"  : "golden rings"};  

key は string でなければならない
Map のコンストラクタを使ったり、Hashable を実装した別のオブジェクトを使えば key の他のタイプを使うことができる
  1. // Map コンストラクタを使う  
  2. var map = new Map();  
  3. map[1] = "partridge";  
  4. map[2] = "turtledoves";  
  5. map[5] = "golden rings";  
value は任意のオブジェクトもしくは null を取ることができる
存在しない key の value を取得しようとすると null が返ってくるが、そもそも value は null を取ることができるので、 containsKey()putIfAbsent() でチェックする
  1. var gifts = { "first""partridge" };  
  2. gifts["fourth"] = "calling birds";  
  3.   
  4. // 長さ  
  5. print(gifts.length);  
  6.   
  7. // 削除  
  8. gifts.remove('first');  
  9.   
  10. // コピー  
  11. var regifts = new Map.from(gifts);  
  12. print(regifts['first']);   

Iterating

forEach()
  1. var gifts = {  
  2.   "first" : "partridge",  
  3.   "second""turtledoves",  
  4.   "fifth" : "golden rings"};  
  5. gifts.forEach((k,v) => print('$k : $v'));  
forEach() で返ってくる key-value ペアの順番は保証されていないので、依存しないようにする

keys や values だけ必要な場合は getKeys(), getValues() でそれぞれの Collection オブジェクトが取得できる
  1. var gifts = {"first""partridge""second""turtledoves"};  
  2. var values = gifts.getValues();  
  3. values.forEach((v) => print(v));  
Map document
Hashable document


■ Functions

=> e シンタックスは {return e;} の短縮形

関数引数を [] を囲むと、オプショナルな引数になる
  1. String say(String from, String msg, [String device]) {  
  2.   var result = "$from says $msg";  
  3.   if (device != null) {  
  4.     result = "$result with a $device";  
  5.   }  
  6.   return result;  
  7. }  
オプショナルな引数に初期値をセットすることもできる
  1. String say(String from, String msg, [String device='carrier pigeon']) {  
  2.   var result = "$from says $msg";  
  3.   if (device != null) {  
  4.     result = "$result with a $device";  
  5.   }  
  6.   return result;  
  7. }  
オプショナルな引数の名前を指定してセットすることもできる
  1. print(say("Bob""Howdy", device: "tin can and string"));  
function をパラメータとして別の function に渡すことができる
  1. bool isOdd(num i) => i % 2 == 1;  
  2. List ages = [1,4,5,7,10,14,21];  
  3. List oddAges = ages.filter(isOdd);  
より短縮したバージョン
  1. List ages = [1,4,5,7,10,14,21];  
  2. List oddAges = ages.filter((i) => i % 2 == 1);  
function を変数に割り当てることもできる
  1. var loudify = (msg) => '!!! ${msg.toUpperCase()} !!!';  
  2. print(loudify('hello'));  
Lexical closures もつくれる
  1. Function makeAdder(num n) {  
  2.   return (num i) => n + i;  
  3. }  
  4.   
  5. main() {  
  6.   var add2 = makeAdder(2);  
  7.   print(add2(3)); // 5  
  8. }  



■ Exceptions
  1. try {  
  2.   breedMoreLlamas();  
  3. catch (final OutOfLlamasException e) {  // a specific exception  
  4.   buyMoreLlamas();  
  5. catch (final Exception e) {             // anything that is an exception  
  6.   print("Unknown exception: $e");  
  7. catch (final e) {                       // no specified type, handles all  
  8.   print("Something really unknown: $e");  
  9. }  
finally
  1. try {  
  2.   breedMoreLlamas();  
  3. catch (final e) {  
  4.   print("Error: $e");  // handle exception first  
  5. finally {  
  6.   cleanLlamaStalls();  // then run finally  
  7. }  
Exception document


■ 参考