2012年4月19日木曜日

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 のインスタンスを取得するとしては以下の方法をよく使います。


0 件のコメント:

コメントを投稿