2011年9月2日金曜日

LayoutInflaterについて勉強する その2

LayoutInflaterについて勉強する その2

Activityが取り扱えるレイアウトはsetContentView()で設定します。この設定によるレイアウト以外のレイアウトを取り扱いたい場合にLayoutInflaterを使います。
LayoutInflaterは、プロジェクト内の任意のXMLファイルの内容(View)を、動的に取り扱えるようにするクラスです。
このクラスにより生成されたViewの在り方について実験しました。結論は次のとおりです。

  1. getLayoutInflater()を何度実行しても、返されるLayoutInflaterオブジェクトは同一のものである。
  2. LayoutInflaterのinflate()が返すViewオブジェクトは、inflate()の実行毎に異なる。このことから、inflate()は、その内部でViewをnewしていると想定される。

この結論を得るために使ったsourceを掲載しておきます。
public class LayoutInflaterActivity extends Activity{
    @Override
    public void onCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);
        
        LayoutInflater inf1, inf2;
        TextView tv1, tv2, tv3, tv4;
        
        setContentView(R.layout.main);
        
        inf1 = getLayoutInflater();
        tv1 = (TextView)inf1.inflate(R.layout.textview, null);
        tv1.setText("inf1a");
        
        tv2 = (TextView)inf1.inflate(R.layout.textview, null);
        tv2.setText("inf1b");
        
        inf2 = getLayoutInflater();
        tv3 = (TextView)inf2.inflate(R.layout.textview, null);
        tv3.setText("inf2a");
        
        tv4 = (TextView)inf2.inflate(R.layout.textview, null);
        tv4.setText("inf2b");
        
        Log.d("Inf", "tv1:" + tv1.getText());
        Log.d("Inf", "tv2:" + tv2.getText());
        Log.d("Inf", "tv3:" + tv3.getText());
        Log.d("Inf", "tv4:" + tv4.getText());
        if(inf1==inf2){
            Log.d("Inf", "Inf1==Inf2");
        }
        else{
            Log.d("Inf", "Inf1!=Inf2");
        }
        if(tv1==tv2){
            Log.d("Inf", "tv1==tv2");
        }
        else{
            Log.d("Inf", "tv1!=tv2");
        }
        if(tv1==tv3){
            Log.d("Inf", "tv1==tv3");
        }
        else{
            Log.d("Inf", "tv1!=tv3");
        }
    }
}

main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/MainView"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
</LinearLayout>

textview.xml
<?xml version="1.0" encoding="UTF-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content" 
    >
</TextView>

ログの出力は次のとおりです。
09-02 01:09:47.375: DEBUG/Inf(960): tv1:inf1a
09-02 01:09:47.375: DEBUG/Inf(960): tv2:inf1b
09-02 01:09:47.385: DEBUG/Inf(960): tv3:inf2a
09-02 01:09:47.385: DEBUG/Inf(960): tv4:inf2b
09-02 01:09:47.385: DEBUG/Inf(960): Inf1==Inf2
09-02 01:09:47.385: DEBUG/Inf(960): tv1!=tv2
09-02 01:09:47.385: DEBUG/Inf(960): tv1!=tv3

2011年9月1日木曜日

LayoutInflaterについて勉強する

LayoutInflaterについて勉強する

この記事は、私が初心者であった時のものです。分かり易く書いたつもりの記事を、LayoutInflaterに掲載しました。(2012/05/05)

LayoutInflaterは、他のxmlリソースのViewを取扱える仕掛けです。
これについて勉強するため、コーディングしましたのでsourceを公開します。

ここに掲載したのは学習・実験を目的にしたものです。まともに動くプログラムは、動的にレイアウトの挿入&削除を繰り返すに掲載してあります。

以下 javaファイル********************
/*
 * LayoutInflaterへの理解を深めるため、ありがちなメソッドfindViewById()と、
 * LayoutInflaterを使った仕掛けとの両方を書きました。
 * LayoutInflaterはxmlファイルを扱えはしますが、そのファイルはリソースにある
 * コンパイルされたファイル(R.something file)を指します。
つまり、物理的に存在するxmlファイルを直接取り扱うのではありません。
例:res>layout>main.xml を設定するのでは無い
R.layout.main を設定します。
 */
public class LayoutInflaterActivity extends Activity{
    @Override
    public void onCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);
     
        LinearLayout ll;
        LayoutInflater inf;
        TextView tv;
/*
LayoutInflaterは、getSystemServiceメソッドから取得する。
getSystemServiceという名前から推察できるように、システムのサービスの一環として
LayoutInflaterが提供される仕掛けとなっている。
getSystemService()を使えばWifiやUSBなどハードウェア関連のサービスも使えるようになる。
たかだか、XMLファイルを扱うだけなのに、何故こんな仕掛けが必要なのでしょうか、という疑問が湧いてくる。
 */
        inf = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
//こんな大げさな仕掛けには違和感を感じる。
getSystemServiceメソッドに目移りをしてしまい、このメソッドの仕掛けを勉強しなければならないような雰囲気になります。
そんな御仁にお勧めなのが次のメソッドだ。
        inf = getLayoutInflater();//初めから、これを出せ。(-_-;)
     
        //main画面をActivityで表示できるようにする。
        setContentView(R.layout.main);//毎度お馴染みのメソッドです。
        ll = (LinearLayout)findViewById(R.id.MainView);
     
        //main画面の中にあるTextViewを取得する。
        tv = (TextView)findViewById(R.id.MainText);//これも毎度お馴染み
        tv.setText("ccc");//文字列の代入に成功する。当たり前。
        //以上は、ありがちなプログラム処理です。
     
        //別のファイルにあるTextViewを取得するように試みる。
        tv = (TextView)findViewById(R.id.SubText);
        if(tv==null){
            //しかし、nullが返される。
            Log.e("InfAct", "return findViewById() : null");
            //普段よく使うfindViewById()は、自分のActivity画面にしか使えないのだ。
            return;
        }
        //上記エラーのため、以下を実行できない。
        tv.setText("ddd");//実行できない無駄なコード
        ll.addView(tv);//実行できない無駄なコード
     
        //もし、上記エラーが無いとして、ここまで来たとした場合,,,,
        //別のxmlファイルにアクセスする。
        tv = (TextView)inf.inflate(R.layout.textview, null);
        tv.setText("eee");//文字列の代入ができる。おお。すごい。
        ll.addView(tv);//main画面に挿入することだってできる。
    }
}

以上 javaファイル********************

main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/MainView"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
    <TextView  
        android:id="@+id/MainText"
        android:layout_width="fill_parent" 
        android:layout_height="wrap_content" 
        android:text="@string/aaa"
        />
</LinearLayout>

textview.xml
<?xml version="1.0" encoding="UTF-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/SubText"
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content" 
    android:text="@string/bbb"
    >
</TextView>

string.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">LayoutInflater</string>
    <string name="aaa">aaa</string>
    <string name="bbb">bbb</string>
</resources>

実験:ViewStubはViewの動的挿入に使えるか

実験:ViewStubはViewの動的挿入に使えるか

結論:「動的挿入」の定義による。
任意のViewを1回だけ挿入するだけなら使える。
挿入・削除を繰り返す目的には使えない。

動作
ViewStubは、実行時に、別のViewを、ViewStubが書かれてあるxml上の位置に挿入するものである。この挿入により、元々あったViewStubコマンドが、挿入するViewにより上書きされてしまうので、1回限りしか実行できない。
なお、挿入したViewを削除するには、setVisibility(View.GONE)を実行すれば良い。つまり、1回だけ挿入&削除はできるのである。

目的
ViewStubの目的は、処理速度の向上にある。
Layout Tricks: Using ViewStubs
Turbo-charge your UI

すまん。「実験」という表題でありながら実験したsourceを掲載していない。私は複数回挿入&削除したかったので、私的にはこの実験は失敗だった。

layout_weightの留意点

layout_weightの留意点

layout_weightは、複数のViewを任意の大きさの割合で配置することができるコマンドであり、とても有益である。このコマンドの留意点は次のとおりである。
  1. 絶対の位置では無く、相対的な位置を設定する。このため、物理画面の大きさの違いに関係無く、全てのサイズの画面に対応できる。
  2. 横方向で使う場合:layout_widthの値を"0dp"にすること。具体的には次のように書く。android:layout_width="0dp"
    この値を"fill_parent"にした場合、layout_weightの数値が低いViewが大きく配置される。物好きな人はお試しください。
    この値を"wrap_content"にした場合、各Viewの配置割合は不正確になる。
  3. 縦方向で使う場合には、(layout_widthを0dpにするのではなく)layout_heightの値を"0dp"にする。
  4. layout_weightで指定した大きさを超える長い文字列がViewに代入された場合、layout_weightの指定が優先適用され、指定された方向にはサイズは伸びない。そして、Viewは、layout_weightによる指定が無い方向に伸びていく。
  5. android:weightSumに代入する値が、各layout_weightの値の合計よりも多い場合、その多い分が余白となる。つまり、 android:weightSumは余白を設けるコマンドである。
  6. android:weightSumは設けなくても良い。この値が、各layout_weightの値の合計と同一であるならば、android:weightSumを書く意味は無い。
  7. LinearLayoutの子Viewにだけしか使えない。ただし、LinearLayoutを継承したViewGroupの子Viewにも使える。例えば、RadioGroupの子ViewであるRadioButtonにも使える。
  8. LayoutInflaterを用いて追加するViewでは、inflater()の第二引数に親のLinearLayoutを指定することによって、layout_weightが有効に機能する。詳細は動的にレイアウトの挿入&削除を繰り返すを見てください。
例:縦方向の場合
画面の上の方に80%の余白を設け、80%よりも下の位置からButtonを配置する場合

    <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:orientation="vertical"
        android:weightSum="100"
        >
        <!-- 位置調整のためのダミーView -->
        <View
            android:layout_width="fill_parent"
            android:layout_height="0dp"
            android:layout_weight="80"
            >
        </View>
        <Button (以下、省略)


例:横方向の場合
android:weightSumの余白作成機能を用いて、Viewを中央に配置することができる。この場合、余白を作成するためのダミーのViewを必要としない。
左の余白が1、SeekBarが8、右の余白が1の割合でSeekBarを中央に配置するxmlのサンプルは次のとおりです。
    <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:weightSum="100"
        android:gravity="center"
        >
        <SeekBar
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="80"
            >
        </SeekBar>
    </LinearLayout>

例:RadioButtonを横方向に均等に並べる場合
    <RadioGroup
        android:id="@+id/radioG_Start"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        >
        <RadioButton
            android:id="@+id/radio_StartZero"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="50"
            android:text="@string/RadioEndVideo"
            >
        </RadioButton>
        <RadioButton
            android:id="@+id/radio_StartFavorite"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="50"
            android:text="@string/SetTime"
            >
        </RadioButton>
    </RadioGroup>

私が実験用に作成したxmlを掲載しておきます。
"Buttoooooooooooon"などと、ボタンに執着があるかのような記述がありますが、これは長い文字列を設定した場合の挙動を実験するためのものであり、私個人の嗜好を示すものでは無いことを念のため申し添えておきます。

Eclipseのxmlエディタの下の方にある「Graphical Layout」タブをクリックすると、ビルドすること無く、画面を確認できます。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical"
    >
    <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        >
        <SeekBar
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="60"
            >
        </SeekBar>
        <TextView
            android:text="Valuuuuuuuuuuuuuuue"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="20"
            >
        </TextView>
        <Button
            android:text="Buttoooooooooooon"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="20"
            >
        </Button>
    </LinearLayout>
    <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:weightSum="120"
        >
        <Button
            android:text="Button"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="60"
            >
        </Button>
        <Button
            android:text="Buttoooooooooooon"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="20"
            >
        </Button>
        <Button
            android:text="Button"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="20"
            >
        </Button>
    </LinearLayout>
</LinearLayout>

【開発環境】
OS:Windows Vista
Eclipse IDE for Java Developers Version: Indigo Release

実験:TableLayoutで均等配置はできるのか


実験:TableLayoutで均等配置はできるのか

結論:できる。横方向だけですけど。

考察:
stretchColumnsは、指定された列を横方向に最大限伸ばす、という機能があります。これを全ての列に適用すれば、均等配置ができます。
各列に格納される文字数が少なければ、各列は均等に配置されます。
しかし、長い文字列が格納されると、均等ではなくなってしまいます。この長い文字列対策のため、android:width="0dp"を書くことにより均等に配置できます。

問題点:
均等に配置するだけであって、任意の割合で表示する機能はstretchColumnsにはありません。任意の割合で表示する場合にはandroid:layout_weightを導入する方が良いと思います。
均等に配置するということを目的に、stretchColumnsが設計されたのではありません。

以下は、実験用の動かないXMLです。SeekBarとLinearLayoutとが1対1の割合で表示されることを期待しましたが、そうなりません。

<TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:stretchColumns="0,1">
    >
    <TableRow>
        <SeekBar
            android:id="@+id/SeekBar"
            android:width="0dp"
            >
        </SeekBar>
        <LinearLayout
            android:orientation="horizontal"
            >
            <TextView
                android:id="@+id/TextView"
                android:text="Valueeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"
                android:width="0dp"
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                >
            </TextView>
            <Button
                android:id="@+id/Button"
                android:text="Buttonnnnnnnnnnnnnnnnnnnn"
                android:width="0dp"
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                >
            </Button>
        </LinearLayout>>
    </TableRow>
</TableLayout>

2011年8月27日土曜日

static変数の値は次回起動へ継承される

static変数の値は次回起動へ継承される

finish()が実行されるタイミングの続きの話題です。
finish()が実行されるとdestroy()が実行されます。しかし、そのアプリのプロセスは生き残っています。DDMSやデバッガでそのことを確認できることは周知のとおりです。

destroyされた後の状態で、エミュレータのアプリの起動アイコンをクリックすると、destroyされたアプリが立ち上がってくるのです!おお!こんなことができるとは知らなかった。デバッガも立ち上がって来るので、finish()したアプリが同一のプロセスで稼働しているということが確認できます。
他のプラットフォームにおける思い込みを捨てて、認識を新たにする必要があります。

そこで、下記の実験用プログラムを作成し、
1.デバッガから実行しました。実行させても、finish()により、直ぐに終了します。
2.そして、エミュレータにある(finish()によるdestroy後の)アプリの起動アイコンをクリックし実行させました。
つまり合計で2回実行させました。

public class StaticActivity extends Activity {
    static String sStatic = "at field";
    String sNoStatic = "at field";
    
    public StaticActivity(){
        Log.d("StaticAct", "at constructor:sStatic=" + sStatic);
        Log.d("StaticAct", "at constructor:sNoStatic=" + sNoStatic);
        sStatic = "at constructor";
        sNoStatic = "at constructor";
    }
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        Log.d("StaticAct", "onCreate:sStatic=" + sStatic);
        Log.d("StaticAct", "onCreate:sNoStatic=" + sNoStatic);
        sStatic = "at onCreate";
        sNoStatic = "at onCreate";
        finish();
    }
}

ログには次のとおり出力されました。
第一回目の出力
08-26 15:36:28.415: DEBUG/StaticAct(5031): at constructor:sStatic=at field
08-26 15:36:34.515: DEBUG/StaticAct(5031): at constructor:sNoStatic=at field
08-26 15:36:38.855: DEBUG/StaticAct(5031): onCreate:sStatic=at constructor
08-26 15:36:38.855: DEBUG/StaticAct(5031): onCreate:sNoStatic=at construct
第二回目の出力
08-26 15:37:16.655: DEBUG/StaticAct(5031): at constructor:sStatic=at onCreate
08-26 15:37:16.655: DEBUG/StaticAct(5031): at constructor:sNoStatic=at field
08-26 15:37:17.207: DEBUG/StaticAct(5031): onCreate:sStatic=at constructor
08-26 15:37:17.207: DEBUG/StaticAct(5031): onCreate:sNoStatic=at constructor

第二回目のログのstatic変数の最初の内容は、第一回目の実行の最後に代入した値("at onCreate")が残ったままになっています。
このことから、adsaria氏が言うように、「次回、再利用する時にはスタティックなメンバ変数の初期化は行ってくれない。」と言えるのです。

このような仕組みを、便利だと捉えるか、危険だと捉えるか、考え方はいろいろありましょうが、いずれにしても留意はしなければなりません。

2011年8月26日金曜日

finish()が実行されるタイミング

finish()が実行されるタイミング

finish()が実行されるタイミングについてはadsaria moodのfinish()ではプロセスは終わらないに書かれているとおりです。
私なりに、私自身が納得できるように、実験用プログラムを作りました。


public class FinishTestActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        ToneGenerator tg;
        tg = new ToneGenerator(AudioManager.STREAM_ALARM, 100);
        tg.startTone(ToneGenerator.TONE_DTMF_1);
        Log.d("FinishTest", "before finish()");
        finish();
        Log.d("FinishTest", "after finish()");
        tg.stopTone();
    }


    @Override
    protected void onDestroy() {
        Log.d("FinishTest", "onDestroy()");
        super.onDestroy();
    }
}

もし、finish()により、いきなりプログラムが終了するのであれば、tg.stopTone()が実行されないことになりますので、ビープ音が鳴り続けます。
しかし、ビープ音は鳴り止みますので、tg.stopTone()は実行されたということです。
ログには次のとおり記録されます。

08-26 11:35:18.325: DEBUG/FinishTest(785): before finish()
08-26 11:35:18.365: DEBUG/FinishTest(785): after finish()
08-26 11:35:19.635: DEBUG/FinishTest(785): onDestroy()

私なりの言葉で説明すると次のとおりになります。
命令の発出の仕方にはsend方式とpost方式があります。
send方式は命令を発出したら、即座にそれを実行する方式です。
post方式は命令を「メッセージ格納器」に入れるだけです。命令を受け取る側は暇になった時に「メッセージ格納器」から命令を取り出して実行するのです。
上記プログラムの例だと、onCreateメソッドの実行が完了した後が、暇になる時です。
finish()はpost方式なのです。


finish()を実行してもプロセスが生きていることは、DDMSやデバッガを使っていれば分かることなので、(内心、変だなと思いつつも)当たり前のようにも感じていました。

finish()ではプロセスは終わらないの記事で重要なのは、次の指摘です。
finish()を呼び出しても、アクティビティはDestroyはされるものの、プロセスとして残っていて(つまりメモリ上にクラスがロードされた状態で残っていて)、次回、再利用する時にはスタティックなメンバ変数の初期化は行ってくれない。
このことから、次の結論が出てきます。
アクティビティ開始時に値が定まっていなければならないメンバ変数の初期化は、宣言時ではなく、onCreate、もしくは必要の応じて、onStart、onResumeで実行する必要がある。
この場合、気になるのが、「次回、再利用する時」です。
onDestroy()した後で、利用者が同一のアプリの起動ボタンを押し下げた時が含まれるのであれば、この事実を周知しなければなりません。実際に役に立つプログラマは少数ですけど。

この続きをstatic変数の値は次回起動へ継承されるに書きました。