2012年3月12日月曜日

複雑な形状のViewの一覧を作成する

複雑な形状のViewの一覧を作成する

ListViewを使えば複雑な形状のViewの一覧を作成できる。
参考:SimpleAdapterのsample code
しかし、この技法の欠点は、書き込み可能なView、例えば、EditTextを配置しても、そのEditTextに対して利用者が書き込みを行うことはできない、ということである。
ListViewのような作り込まれたものは、プログラミングが終わった後で、予期しない不都合が判明する。

素直に、ScrollView & LayoutInflaterを使って、地道に作成する。

ただし、この場合、EditTextにはIDを与えてはいけない、という点に留意しなければならない。
例えば、次のxmlのように、android:id="@+id/EditText"を設けてはいけない。削除しよう。
    <EditText
        android:id="@+id/EditText"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        >
    </EditText>

仮に、IDを設けたとしても、初回起動時には、上手く表示される。しかし、configuration changeに伴う再起動が発生した後で表示されるEditTextには、全て同一の文字列が代入されてしまう。
この問題を避けるために、ID指定を書いてはいけないのである。
TextViewの場合はID指定をしても問題は発生しない。

main.xmlは次のとおりである。IDがParentListのLinearLayoutに、複数の子Viewを動的に追加する。
<?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"
    >
    <ScrollView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        >
        <LinearLayout
            android:id="@+id/ParentList"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            >
        </LinearLayout>
    </ScrollView>>
</LinearLayout>

上記のxmlの中に、次のlistitem.xmlを動的に挿入する。各ViewにはIDを付けていない。
<?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="wrap_content"
    android:padding="2dp"
    android:background="#555"
    >
    <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:background="#000"
        >
        <CheckBox
            android:text="@string/Apply"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            >
        </CheckBox>
        <LinearLayout
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            >
            <TextView
                android:text="@string/Before"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
            >
            </TextView>
            <EditText
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                >
            </EditText>
        </LinearLayout>
        <LinearLayout
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            >
            <TextView
                android:text="@string/After"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
            >
            </TextView>
           <EditText
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                >
            </EditText>
        </LinearLayout>
    </LinearLayout>
</LinearLayout>

上記2個のxmlを使って、複数のViewを動的に生成し、一覧を作成するJavaのcodeは次のとおりである。ViewのIDが存在しないため、個々のViewの識別はアドレス(相対位置と表現して良いかもしれない)で行う。
public class InflateListActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        getWindow().setSoftInputMode(LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN); 
        setContentView(R.layout.main);
        
        int i;
        ArrayList<String> al;
        BackupData bd;
        
        bd = (BackupData)getLastNonConfigurationInstance();
        if(bd==null){
            //サンプルプログラムなので適当にデータを代入する。
            al = new ArrayList<String>();
            al.add("true");
            al.add("Green");
            al.add("Blue");
            al.add("false");
            al.add("Green");
            al.add("Blue");
            al.add("true");
            al.add("Green");
            al.add("Blue");
            al.add("false");
            al.add("Red");
            al.add("Blue");
            al.add("true");
            al.add("Yellow");
            al.add("Red");
        }
        else{//configuration changeに伴う再起動
            al = bd.getBackupData();
        }
        
        for(i=0; i<al.size()/3; i++){
            LayoutInflater inf;
            LinearLayout llParent, ll, ll2, llChild;
            CheckBox cb;
            EditText et;
            String s;
            
            //親Viewを取得する。
            llParent = (LinearLayout)findViewById(R.id.ParentList);
            
            //挿入する子Viewを作成する。
            inf = getLayoutInflater();
            ll = (LinearLayout)inf.inflate(R.layout.listitem, null);
            
            ll2 = (LinearLayout)ll.getChildAt(0);
            
            //子Viewにデータを入れる。
            cb = (CheckBox)ll2.getChildAt(0);
            s = al.get(i*3);
            if(s.equals("true")==true) cb.setChecked(true);
            else cb.setChecked(false);
            
            llChild = (LinearLayout)ll2.getChildAt(1);
            et = (EditText)llChild.getChildAt(1);
            et.setText(al.get(i*3+1));
            
            llChild = (LinearLayout)ll2.getChildAt(2);
            et = (EditText)llChild.getChildAt(1);
            et.setText(al.get(i*3+2));
            
            //親Viewに子Viewを入れる。
            llParent.addView(ll);
        }
    }
    
    //configuration changeに伴う再起動への対応
    @Override
    public Object onRetainNonConfigurationInstance(){
        ArrayList<String> al;
        BackupData bd;
        LinearLayout llParent;
        int i, iSize;
        
        al = new ArrayList<String>();
        llParent = (LinearLayout)findViewById(R.id.ParentList);
        iSize = llParent.getChildCount();
        for(i=0; i<iSize; i++){
            LinearLayout ll, ll2, llChild;
            CheckBox cb;
            EditText et;
            String s;
            
            ll = (LinearLayout)llParent.getChildAt(i);
            ll2 = (LinearLayout)ll.getChildAt(0);
            
            cb = (CheckBox)ll2.getChildAt(0);
            if(cb.isChecked()==true) s = "true";
            else s = "false";
            al.add(s);
            
            llChild = (LinearLayout)ll2.getChildAt(1);
            et = (EditText)llChild.getChildAt(1);
            s = et.getText().toString();
            al.add(s);
            
            llChild = (LinearLayout)ll2.getChildAt(2);
            et = (EditText)llChild.getChildAt(1);
            s = et.getText().toString();
            al.add(s);
        }
        
        bd = new BackupData();
        bd.setBackupData(al);
        return bd;
    }
    
    private class BackupData{
        private ArrayList<String> dataBackup;
        void setBackupData(ArrayList<String> al){
            dataBackup = al;
        }
        ArrayList<String> getBackupData(){
            return dataBackup;
        }
    }
}



2012年2月6日月曜日

稼働環境のバージョンを知る

稼働環境のバージョンを知る

稼働中の端末のPlatform(API level・バージョン)を取得するでも触れた、Build.VERSIONについて調べた。

結論は次のとおり。
我々のような一般的なプログラマから見て、実用的なのは、Build.VERSION.SDK_INTだけである。これ以外の定数を使う機会はない。

Build.VERSIONは稼働しているAndroidのバージョンを知るクラスである。自分のアプリのバージョンを知る道具ではない。

エミュレータの4.0.3上での実行結果は次のとおりであった。
Build.VERSION.CODENAME : REL
Build.VERSION.INCREMENTAL : 237985
Build.VERSION.RELEASE : 4.0.3
Build.VERSION.CODENAME : ICE_CREAM_SANDWICH_MR1

sample codeは次のとおりである。
String s, s1, s2, s3;
TextView tv;

s1 = "Build.VERSION.CODENAME : " + Build.VERSION.CODENAME +"\n";
s2 = "Build.VERSION.INCREMENTAL : " + Build.VERSION.INCREMENTAL +"\n";
s3 = "Build.VERSION.RELEASE : " + Build.VERSION.RELEASE +"\n";
switch(Build.VERSION.SDK_INT){
case Build.VERSION_CODES.DONUT: s = "DONUT"; break;
case Build.VERSION_CODES.ECLAIR: s = "ECLAIR"; break;
case Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1: s = "ICE_CREAM_SANDWICH_MR1"; break;
default: s = "unknown";
}
s = s1 + s2 + s3 + "Build.VERSION.CODENAME : " + s;

tv = (TextView)findViewById(R.id.Text);
tv.setText(s);

2012年2月5日日曜日

稼働中の端末のPlatform(API level・バージョン) を取得する

稼働中の端末のPlatform(API level・バージョン) を取得する

稼働しているPlatform(API level・バージョン)によって、動かなくなる機能があります。このため、自分のアプリが稼働している端末のPlatformを知る必要があります。

稼働しているPlatformによって動かなくなる例として、『「本体設定」の「音の設定」画面の表示』を挙げることができます。

概要は、「本体設定」の「音の設定」画面を表示するに書いておきました。その記事では、「Platform 2.2の場合」と「Platform 1.6の場合」を、startActivityForResultが発生するエラーによって切り分けていました。これは「エラーになるんだな」という学習にはなりますが、適切なcodeではありません。
稼働中のPlatformを取得し、そのPlatform毎に対応するのが適切です。

この問題の解決には次のサイトを見ながらcodingしていけば良いです。
Build.VERSION_CODES

sample codeは下記のとおりです。
public class SettingsActivity extends Activity
    implements
    OnClickListener
    {
    final String TAG = "me";
    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){
            Intent it;
            
            if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.ECLAIR){
                //Platform 2.0以上の場合
                Log.d(TAG, "Platform 2.0 and later");
                it = new Intent(Settings.ACTION_SOUND_SETTINGS);
            }
            else{
                Log.d(TAG, "Platform 2.0 before");
                it = new Intent(Settings.ACTION_DISPLAY_SETTINGS);
            }
            try{
                startActivityForResult(it, 0);
            }
            catch(ActivityNotFoundException e){
                Log.e(TAG, "ActivityNotFoundException");
            }
        }
    }
}

上記例では、Platformが2.0以上か未満かを判別しています。エミュレータをPlatform毎に用意して試してみれば良いでしょう。

なお、Platformの判別を適切に行い、かつPlatformが1.6でも稼働するアプリを作るには、次の措置を講じる必要があります。
  1. Android SDKを最新版にする。
  2. Eclipseのメニューコマンド>Project>Properties>Android>Project Build Target>platformを最新(最高)のものを選択する。
  3. AndroidManifest.xmlに次の1行を追加する。
<uses-sdk android:minSdkVersion="4"/>

上記2に書いたように、Project Build Target>platformを最新版にすることにより、codeの中でBuild.VERSION_CODES.ICE_CREAM_SANDWICHという定数を書くことができます(コンパイルエラーにならないようになります)。
上記3の措置を講じることにより、Platformが1.6でも無事稼働するようになります。

2012年1月30日月曜日

paddingの留意点

paddingの留意点

副題: CheckBox bug

この記事の続編をRadioButtonへのpaddingに書きました。(2012/05/09)

この記事ではCheckBoxを題材にしていますが、他のViewでも同じ趣旨のことは言えると思われます。

そして、また、この記事ではpaddingを題材にしていますが、背景色(background)の設定でも同じことが言えると思われます。

既に次のサイトでもお馴染みのCheckBoxに関する不都合です。
Android - Spacing between CheckBox and text
Is that is a bug with CheckBox in android?

CheckBoxに対してpaddingを施すと、CheckBoxの中の「文字列」と「チェックの画像」とが重なってしまう、という現象が発生します。

ここで言う、「paddingを施す」というのは、CheckBoxを定義するxmlでpaddingを施すことと、Javaのcodeでpaddingを施すことの両方を言います。

教科書どおりにCheckBoxを定義したxmlは次のとおりです。これだけだと何の問題もありません。
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    >
    <CheckBox
        android:id="@+id/CheckBox"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/hello"
        >
    </CheckBox>
</LinearLayout>

上記xmlを使って次のJava codeを実行すると変になる。
public class CheckBoxActivity extends Activity{
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        CheckBox cb;
        cb = (CheckBox)findViewById(R.id.CheckBox);
        cb.setPadding(5,5,5,5);
    }
}

xmlにpaddingを入れても変になる
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    >
    <CheckBox
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/hello"
        android:padding="5dp"
        >
    </CheckBox>
</LinearLayout>

解決方法として、次の4通りが考えられます。
  1. CheckBoxに対してpaddingを施すことをやめる。(-_-;)
  2. 変になることを考慮に入れて、CheckBoxの左側にpaddingを多めに施す。
  3. 文字列はTextViewで表示する。
  4. CheckBoxの上位にLinearLayout等のViewGroupを設け、このViewGroupに対してpaddingを施す。
このバグは今後修正されるのでしょうか。この問題が提起されたのが2010年10月ですので、ずっと放置されたままですね。むむむ!
今後修正される可能性があるとするならば、上記2の方法を講じることは避けた方がよろしいです。
「修正なんかあり得る訳が無い!」という確信のある御仁は、上記2の方法でも良いかもしれません。そして、その確信は、今まさに、現実のものとして実現されています。この現実は、今後もずっと続くでしょう。この確信者は、先見の明があると言って良いかもしれません。(-_-;)

しかし、Androidの設計者を信奉する迷える子羊は、修正があることを疑いません。
そこで、上記3の方法を講じたxmlを次に掲げておきます。文字列の縦位置をチェックボックス画像の中央に位置付けるのが重要です。

<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    >
    <CheckBox
        android:id="@+id/CheckBox"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_centerVertical="true"
        android:padding="5dp"
        >
    </CheckBox>
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@id/CheckBox"
        android:text="長い、長い、とても長い文字列が、ドンブラコ ドンブラコ と流れてきました。 TextView は処理をしきれずに落ちてしまいました。おしまい。"
        >
    </TextView>
</RelativeLayout>

Viewに対してpaddingを施すと変になるって記事は、既に書いておきました。この記事を読んで頂ければ、上記第4番目の対処方法が分かると思います。

2012年1月29日日曜日

xml:文字列の前後の空白文字が表示されない

xml:文字列の前後の空白文字が表示されない

画面構成を整えるため、文字列の前後に空白文字を挿入したい場合がある。
英語の環境で次のxmlを実行させた場合、文字列の前後の空白文字は表示されない。
    <string name="TextVolume"> Volume </string>
空白文字を表示するためには、次のように、前後を括弧で括らねばならない。
    <string name="TextVolume">" Volume "</string>

日本語の環境において、文字列の前後に全角の空白文字を書いた場合、その全角空白文字は表示される。
    <string name="TextVolume"> 音 量 </string>
しかし、半角の空白文字であれば、その空白文字は表示されない。この場合、前後に括弧を施せば表示される。

これらはxmlでの話である。Javaのcodeにおいては、普通に、教科書どおりに書けば良い。

2012年1月27日金曜日

xml:勝手に空白文字が挿入されてしまう

xml:勝手に空白文字が挿入されてしまう

次のxmlがある。この文字列を画面に表示すると、「春」から「夏」にかけて、改行はされる。しかし、その部分に、空白文字が1個挿入されてしまうのである。なので、「夏」が1個右にズレて表示される。

<string name="AboutThis3">
春\n

</string>

これは、「春\n」と「夏」の間に、(エディタ上では見えない)改行文字が挿入されているために発生する現象である。
そして、「春\n」の前、及び「夏」の後にも、各々1個の(エディタ上では見えない)改行文字が挿入されている。
この(エディタ上では見えない)改行文字が空白文字に置き換えられるのである。
なので、この(エディタ上では見えない)改行文字を書かないようにすれば良い。解決策は次のとおりである。

<string name="AboutThis3">春\n夏</string>

(エディタ上では見えない)改行文字が空白文字に変換されてしまう仕様は、英語を記述することを想定しているためのようである。日本語しか使わないのであれば、違和感のある仕様だ。
参考:第10回 落とし穴になる空白文字と改行文字

上記例では短い文字列であったため問題は無い。
しかし、日本語で比較的長い文字列を書く場合、エディタ上で読み難くなる。

<string name="AboutThis3">【動作環境】android 2.2\n  【ソフトウェア種別】フリーソフトウェア\n【制作者名】Androyer\n 読み難いぞ(T_T)</string>

エディタ上で読み難くなるという、ただそれだけであるため、問題は無い。
しかし、やっぱり見易くしたいという御仁もいらっしゃるであろう。その場合、やむを得ないが、括弧で括り、文字列の先頭に半角空白文字を挿入して頭を揃えることになる。

    <string name="AboutThis3">
        " 【動作環境】android 2.2\n"
        "【ソフトウェア種別】フリーソフトウェア\n"
        "【制作者名】Androyer"
    </string>

第1列目が空白文字で表示されてしまうが、ご容赦して頂くことになる。このことについて、文句を言う利用者はいないであろう。

2012年1月12日木曜日

実験:configuration changeは、画面の無いActivityでも発生するのか

実験:configuration changeは、画面の無いActivityでも発生するのか

本実験の趣旨
世間に出回っている多くのアプリの共有機能は、(Serviceに対してではなく)Activityに対して共有するものある。
拙作プログラムのActivityは、ゴチャゴチャしており、これ以上何か機能を追加したくは無い。
このため、共有を受け付けるためだけのActivityを作成することにした。共有を受け付けるためだけであるため、画面を表示してはいけない。そこで、画面の無いActivityが必要になった。

画面の無いActivityとは、AndroidManifest.xmlにおいて、次の行を追加したものです。
android:theme="@android:style/Theme.Translucent.NoTitleBar"

画面が表示されない状態であるから、端末を縦位置から横位置へ、又は横位置から縦位置へ変更したとしても、configuration changeは発生しないと想定される。果たしてどうなのか。

結果
上記設定による画面を持たないActivityにおいて、端末の位置を変更させた場合、onRetainNonConfigurationInstanceメソッドが実行され、Activityは再起動する。
つまり、configuration changeが発生するということである。

追記(重要)
上記は、2012年1月時点の結果であった。
2012年5月1日に、この実験を行おうとした。onRetainNonConfigurationInstanceメソッドはdeprecatedであるとされていた。このため、onSaveInstanceStateメソッドでconfiguration changeの発生を捕捉しようとした。しかし、onSaveInstanceStateメソッドでは、画面の無いActivityのconfiguration changeを捕捉できなかった。

このように、1月時点と、5月時点での結果が異なっていたので、詳しく実験をしてみた。
参考:透明のActivityにはconfiguration changeが発生しない件