top page‎ > ‎

LayoutInflater

LayoutInflater むむむ!


2012年5月5日(土)

Androidのプログラミングでは、画面のViewを構成する場合、layout用xmlファイルを用いて、レイアウトを作成することが推奨されている。
しかし、xmlファイルを用いた場合、それは、静的にレイアウトが決定付けられてしまい、プログラムの実行時にレイアウトを変形することはできない。
この不都合を回避するため、LayoutInflater Classが用意されており、このクラスを活用することにより、実行時にお好みのレイアウトに変形できる。
ただし、クセがあるため、ここに私なりの留意事項を書いておく。

基礎知識

Java codeだけで動的にViewを設ける場合、例えば、TextViewを動的に生成して使う場合、次のとおりJavaでcodingする。
TextView tv = new TextView(this);
生成すべきViewが単体であればこれで良いし、わざわざLayoutInflaterを用いる必要は無い。

しかし、例えば、LinearLayoutの下にTextViewやButton等の複数のViewを設ける場合等、複雑な形状のViewを生成する場合、それをJavaだけでcodingすると可読性・保守性が低下する可能性が高い。

そこで、実行時に追加すべきレイアウトを(静的に)xmlファイルに定義しておき、実行時にLayoutInflaterを用いて、そのxmlで定義したレイアウトを、任意の(別の)レイアウトに追加してやるのである。

この場合、「動的に追加すべきレイアウト」と「追加する位置」との関係性が課題となる。

何度も追加すると、同じレイアウトが何個も表示されてしまって不都合が生じる。このため、追加する直前に、既に追加されているレイアウトを削除する手順が必要である。

留意点 その1:newをする必要は無い

Javaだけでcodingをする場合、Viewを生成するのにnewを使う。
しかし、LayoutInflaterを用いると、LayoutInflaterの内部でnewが行われる(と思われる)ので、プログラマーはnewをしなくても良い。

動的に追加すべきレイアウト

動的に追加すべきレイアウトのxmlの見本は次のとおりである。普通にレイアウト用のxmlを作成すれば良い。
一般的に言えば、追加すべきレイアウトは、1種類だけではない。1種類だけであれば、LayoutInflaterを使わずに、始めからxmlに盛込んでおけば良いのである。追加すべきレイアウトが複数あるからこそ、それらを相互に適宜入れ替えることができるのである。
ここではTextViewの例だけを示したが、他の形状(例えば、Button)のレイアウト用xmlを設けるのが一般的かもしれない。
ただし、1個のレイアウトだけを挿入・削除したいだけであれば、1種類だけで良い。
res>Layout>textview.xmlという名前で作成する。
<?xml version="1.0" encoding="UTF-8"?>
<TextView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/TextView"
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content" 
    >
</TextView>

追加する位置

追加する位置のレイアウトのxmlの見本は次のとおりである。普通にレイアウト用のxmlを作成すれば良い。
一般論としては、追加する位置のViewはLinearLayoutにしておいた方が無難だ。LinearLayout ClassにはaddViewメソッドやremoveAllViewsメソッドがあり、これらのメソッドを使えば簡単に動的に追加・削除ができる。
次の見本のxmlでは、id/ParentのLinearLayoutに、上記のid/TextViewのTextViewを追加することを想定している。
res>layout>main.xmlという名前で作成する。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    >
    <Button
        android:id="@+id/Button"
        android:text="@string/Button"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        >
    </Button>
    <LinearLayout
        android:id="@+id/Parent"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        >
    </LinearLayout>
</LinearLayout>

Javaのcode

public class LayoutInflaterActivity extends Activity
    implements
    OnClickListener
    {
    Button bu;
    @Override
    public void onCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        bu = (Button)findViewById(R.id.Button);
        bu.setOnClickListener(this);
    }
    
    @Override
    public void onClick(View v) {
        if(v==bu){//ボタンをクリックした場合
            //子Viewが、挿入されたり、削除されたりする。
            LinearLayout ll;
            ll = (LinearLayout)findViewById(R.id.Parent);
            if(ll.getChildCount()>0){//子Viewが存在する場合
                ll.removeAllViews();//動的に削除する。
            }
            else{//子Viewが存在しない場合
                LayoutInflater inf;
                TextView tv;
                
                inf = getLayoutInflater();
                
                //第一引数には、レイアウトファイル名を設定する。
                //tv = (TextView)inf.inflate(R.layout.textview, null);
                //ll.addView(tv);//LinearLayoutのメソッドを使って子Viewを追加する。
                
                //上記2行を次の1行にして書くことも可能である。
                inf.inflate(R.layout.textview, ll);
            }
            return;
        }
    }
}

追加したViewを取り扱う。

追加したTextViewに文字列を設定する例は次のとおりである。
                LayoutInflater inf;
                TextView tv;
                
                inf = getLayoutInflater();
                
                //第一引数には、レイアウトファイル名を設定する。
                //tv = (TextView)inf.inflate(R.layout.textview, null);
                //ll.addView(tv);//LinearLayoutのメソッドを使って子Viewを追加する。
                
                //上記2行を次のように書くことも可能である。
                inf.inflate(R.layout.textview, ll);
                tv = (TextView)findViewById(R.id.TextView);
                
                tv.setText(R.string.app_name);//文字列を設定する。

留意点 その2 むむむ!

上記のcodeで充分間に合うので、この留意点を読む必要は無い。
この記事を書く目的は、この留意点を書くためであった。

inflateメソッドの第二引数にnull以外の値を設定した場合(つまりViewGroupを設定した場合)、inflateメソッドの戻値には、第二引数に設定した値が、そのまま代入される。むむむ!

inflateメソッドの第二引数にnullを設定した場合、戻値は、第一引数で設定した(その結果、生成された)Viewであるため、第二引数にnull以外の値を設定した場合も同じように、生成されたViewの値が戻ることを期待してしまう。しかし、この期待は裏切られ、nullを設定することから学習を始めたプログラマーは戸惑うのである。

親のViewは1個しか存在しないし、getParentメソッドで取得できる。子のViewが複数存在する場合、その中から特定の1個を指定することは困難が伴う場合がある。例えば、子のViewにIDを割り当てていない場合だ。IDを使わずに任意の子Viewを特定するという技法に制約が課されてしまう。
第二引数にnullを設定するかどうかで、戻値が、親Viewである場合と子Viewである場合とに分かれるという仕様に不自然さを感じる。
親のViewを戻値にしてしまったことは設計ミスであると考えるのは果たして私だけであろうか。戻値は、全て、生成した子viewにすべきだった。

android developersのLayoutInflaterのpublic View inflate (int resource, ViewGroup root)には、その戻値について次のとおり書かれてある。
Returns
  • The root View of the inflated hierarchy. If root was supplied, this is the root View; otherwise it is the root of the inflated XML file.
この説明文は簡単に書きすぎていて、何がなんだかわからん。
このような仕様であるため、第二引数にViewGroupを設定した場合、戻値を使う必要はなく、第二引数に設定した値を継続して使えば良いのである。

下記の例の場合、事前にremoveAllViewsメソッドを実行済みであるため、getChildAtメソッドの引数にゼロを設定することができる。

                LayoutInflater inf;
                TextView tv;
                
                inf = getLayoutInflater();
                
                //第一引数には、レイアウトファイル名を設定する。
                //tv = (TextView)inf.inflate(R.layout.textview, null);
                //ll.addView(tv);//LinearLayoutのメソッドを使って子Viewを追加する。
                
                //上記2行を次のように書くことも可能である。
                //inf.inflate(R.layout.textview, ll);
                //tv = (TextView)findViewById(R.id.TextView);
                
                //上記2行を次のように書くことも可能である。
                //リソースIDを使わずに任意の子Viewを特定する技法である。
                //inflateメソッドの戻値を使うことには、事実上意味は無い、、、
                //が、その意味の無いことをやってみる。
                ll = (LinearLayout)inf.inflate(R.layout.textview, ll);
                tv = (TextView)ll.getChildAt(0);
                
                tv.setText(R.string.app_name);//文字列を設定する。


Comments