2012年11月7日水曜日

Microsoft Retiring Messenger, Replacing It With Skype

Windows Live Messenger will be shut down by March 2013, after nearly 13 years of service, so Microsoft can focus its efforts on Skype, its recent $8.5bn acquisition. No word on whether users will be able to transfer their WLM accounts to Skype. 'According to internet analysis firm Comscore, WLM still had more than double the number of Skype's instant messenger facility at the start of this year and was second only in popularity to Yahoo Messenger. But the report suggested WLM's US audience had fallen to 8.3 million unique users, representing a 48% drop year-on-year. By contrast, the number of people using Skype to instant message each other grew over the period.'

Gate One 1.1 Released: Run Vim In Your Browser

"Version 1.1 of Gate One (HTML5 terminal emulator/SSH client) was just released (download). New features include security enhancements, major performance improvements, mobile browser support, improved terminal emulation, automatic syntax highlighting of syslog messages, PDFs can now be captured/displayed just like images, Python 3 support, Internet Explorer (10) support, and quite a lot more (full release notes). There's also a new demo where you can try out vim in your browser, play terminal games (nethack, vitetris, adventure, zangband, battlestar, greed, robotfindskitten, and hangman), surf the web in lynx, and a useful suite of IPv6-enabled network tools (ping, traceroute, nmap, dig, and a domain name checker)."

2012年11月6日火曜日

FrameLayoutの中のViewの位置を揃える

下記のサンプルではTextViewの位置が、Spinnerの縦中央になるようにしている。これを行わない場合、TextViewの位置は、画面の上部にくっついたものとなり、TextViewとRadioButtonの間が空きすぎてしまい、不自然に見える。
FrameLayoutのメンバーViewであるLinearLayoutにandroid:layout_gravity="center_vertical"を加える。
<?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"
>
<FrameLayout
android:layout_width="fill_parent"
android:Layout_height="wrap_content"
>
<Spinner
android:id="@+id/Spinner"
android:layout_width="fill_parent"
android:Layout_height="wrap_content"
>
</Spinner>
<LinearLayout
android:id="@+id/LinearLayout"
android:layout_width="fill_parent"
android:Layout_height="wrap_content"
android:layout_gravity="center_vertical"
>
<TextView
android:id="@+id/TextView"
android:text="@string/noData"
android:layout_width="fill_parent"
android:Layout_height="wrap_content"
>
</TextView>
</LinearLayout>
</FrameLayout>
<RadioGroup
android:id="@+id/RadioGroup"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
>
<RadioButton
android:id="@+id/Button0"
android:text="@string/Button0"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
>
</RadioButton>
<RadioButton
android:id="@+id/Button1"
android:text="@string/Button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
>
</RadioButton>
<RadioButton
android:id="@+id/Button2"
android:text="@string/Button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
>
</RadioButton>
</RadioGroup>
</LinearLayout>

public class FrameLayoutTestActivity extends Activity
implements
OnCheckedChangeListener
{
RadioGroup rg;
Spinner sp;
LinearLayout ll;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
ArrayAdapter<CharSequence> adapter;

ll = (LinearLayout)findViewById(R.id.LinearLayout);
sp = (Spinner)findViewById(R.id.Spinner);
rg = (RadioGroup)findViewById(R.id.RadioGroup);
rg.setOnCheckedChangeListener(this);
rg.check(R.id.Button0);
sp.setVisibility(View.INVISIBLE);
ll.setVisibility(View.VISIBLE);

adapter = ArrayAdapter.createFromResource(
this, R.array.Array,
android.R.layout.simple_spinner_item
);
adapter.setDropDownViewResource(
android.R.layout.simple_spinner_dropdown_item
);
sp.setAdapter(adapter);
}

@Override
public void onCheckedChanged(RadioGroup group, int checkedId){
if(group==rg){
TextView tv;
tv = (TextView)findViewById(R.id.TextView);
switch(checkedId){
case R.id.Button0:
ll.setVisibility(View.VISIBLE);
sp.setVisibility(View.INVISIBLE);
tv.setText(R.string.noData);
break;
case R.id.Button1:
ll.setVisibility(View.VISIBLE);
sp.setVisibility(View.INVISIBLE);
tv.setText(R.string.OneItem);
break;
case R.id.Button2:
sp.setVisibility(View.VISIBLE);
ll.setVisibility(View.INVISIBLE);
break;
}
}
}
}

<?xml version="1.0" encoding="UTF-8"?>
<resources>
<string-array name="Array">
<item>dog</item>
<item>cat</item>
<item>mouse</item>
<item>worm</item>
</string-array>
</resources>

UIコンポーネント/TableLayout

継承するXML属性      
android.view.Viewの属性を継承します。    
android.view.ViewGroupの属性を継承します。       
android.widget.LinearLayoutの属性を継承します。  
 
XML属性  
属性      対応メソッド  概要     
android:collapseColumns setColumnCollapsed(int,boolean) 折り畳む列を指定します。   
android:shrinkColumns   setShrinkAllColumns(boolean)    縮小する列を指定します。   
android:stretchColumns  setStretchAllColumns(boolean)   幅を拡張できる列を指定します。
 
 
XML属性
 
 
 
指定した列を折り畳みます。列は"1, 2, 5"のようにカンマ区切りで指定します。
不正な値、重複値は無視されます。
指定値     デフォルト   動作     
<インデックス値>       -       指定列を折り畳みます。    
 
このタイプの値を含めている他のリソース、テーマ属性の参照を指定します。
または、文字列で指定します。'\\;'はエスケープ文字列('\\n'、'\\uxxxx'ユニコード文字など)。
 
 
 
  _____  
 
指定したインデックス列の横幅を縮小します。列は"1, 2, 5"のようにカンマ区切りで指定します。
不正な値、重複値は無視されます。stretchColumnsと併用可能です。
文字列で指定します。'\\;'はエスケープ文字列('\\n'、'\\uxxxx'ユニコード文字など)。
指定値     デフォルト   動作     
<インデックス値>               指定列を縮小可能にします。  
*               全ての列を縮小可能にします。 
 
または、このタイプの値を含めている他のリソース、テーマ属性の参照を指定します。
 
 
 
  _____  
 
指定したインデックス列の横幅を拡張します。列は"1, 2, 5"のようにカンマ区切りで指定します。
不正な値、重複値は無視されます。shrinkColumnsと併用可能です。
文字列で指定します。'\\;'はエスケープ文字列('\\n'、'\\uxxxx'ユニコード文字など)。
指定値     デフォルト   動作     
<インデックス値>               指定列の横幅を拡張可能にします。       
*               全ての列の横幅を拡張可能にします。      
 
または、このタイプの値を含めている他のリソース、テーマ属性の参照を指定します。
 
 
 

UIコンポーネントRelativeLayout

SUMMARY
 
継承するXML属性      
android.view.Viewの属性を継承します。    
android.view.ViewGroupの属性を継承します。       
 
XML属性  
属性      対応メソッド  概要     
android:gravity setGravity(int) オブジェクトの配置方法を指定します。     
android:ignoreGravity   setIgnoreGravity(int)   gravityで指定した配置方法の影響を受けないWidgetを指定します。  
 
 
XML属性 
 
 
 
オブジェクトの配置方法を指定します。
"|"で区切って複数指定することが可能です。
指定値     デフォルト   動作     
top             コンテナの上部に配置し、サイズ変更は行いません。       
bottom          コンテナの下部に配置し、サイズ変更は行いません。       
left            コンテナの左側に配置し、サイズ変更は行いません。       
right           コンテナの右側に配置し、サイズ変更は行いません。       
center_vertical         上下中央に配置し、サイズ変更は行いません。  
fill_vertical           オブジェクトの高さを、コンテナのサイズに合わせます。     
center_horizontal               左右中央に配置し、サイズ変更は行いません。  
fill_horizontal         オブジェクトの幅を、コンテナのサイズに合わせます。      
center          上下左右中央に配置し、サイズ変更は行いません。
fill    ○       オブジェクトの高さ・幅を、コンテナのサイズに合わせます。   
clip_vertical           top/bottomの追加オプションとして、オブジェクトの上部/下部の境界をコンテナの境界に合わせます。   
clip_horizontal         left/rightの追加オプションとして、オブジェクトの左側/右側の境界をコンテナの境界に合わせます。   
 
  _____  
 
gravityで指定した配置方法の影響を受けないWidgetを指定します。
 
 
  • サンプルコード
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
      android:id="@+id/TestSample"
      android:ignoreGravity="@+id/TEV"
      android:gravity="center"
      android:layout_width="fill_parent"
      android:layout_height="fill_parent">
      <Button
        android:id="@+id/continue_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Button"/>
        <TextView
          android:id="@+id/TEV"
          android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:textColor="#ff0000"
          android:text="TextView"/>
</RelativeLayout>
 
結果
 
 

UIコンポーネント/LinearLayout

継承するXML属性      
android.view.Viewの属性を継承します。    
android.view.ViewGroupの属性を継承します。       
 
XML属性  
属性      対応メソッド  概要     
android:baselineAligned setBaselineAligned(boolean)     ベースラインに合わせて配置するかどうかを指定します。     
android:baselineAlignedChildIndex       setBaselineAlignedChildIndex(int)       ベースラインとなる属性を持ったWidgetのインデックスを指定します。    
android:gravity setGravity(int) オブジェクトの配置方法を指定します。     
android:orientation     setOrientation(int)     配置する向きを指定します。  
 
 
XML属性
 
 
 
レイアウト内の各Widgetのベースラインに合わせて配置するかどうかを指定します。
指定値     デフォルト   動作     
true    ○       レイアウト内の各Widgetのベースラインに合わせて配置します。       
false           レイアウト内の各Widgetのベースラインに合わせて配置しません。      
 
このタイプの値を含めている他のリソース、テーマ属性の参照を指定します。
 
 
  • サンプルコード
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
            android:id="@+id/TestSample"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent"
            android:baselineAligned="true"
            android:orientation="horizontal">
    <TextView android:id="@+id/Text1"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:text="name"> </TextView>
    <EditText android:id="@+id/EText1"
              android:layout_height="wrap_content"
              android:layout_width="fill_parent"/>
</LinearLayout>
 
結果
 
 
  _____  
 
レイアウト内のベースラインとなる属性を持ったWidgetがどこにあるのか、
追加した順番のインデックスで指定します。
 
このタイプの値を含めている他のリソース、テーマ属性の参照を指定します。
 
 
  • サンプルコード
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:baselineAligned="true">
    <TextView
            android:layout_height="40dip"
            android:layout_width="wrap_content"
            android:text="text"/>
    <EditText
            android:layout_height="50dip"
            android:layout_width="wrap_content"/>
    <Button
            android:layout_height="60dip"
            android:layout_width="wrap_content"
            android:text="Bu"/>
    <LinearLayout
            android:baselineAlignedChildIndex="2"
            android:orientation="vertical"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent">
        <TextView
            android:layout_height="30dip"
            android:layout_width="wrap_content"
            android:text="text2"/>
        <EditText
            android:layout_height="60dip"
            android:layout_width="wrap_content"/>
        <Button
            android:layout_height="90dip"
            android:layout_width="wrap_content"
            android:text="Bu2"/>
    </LinearLayout>
</LinearLayout>
 
結果
 
 
  _____  
 
オブジェクトの配置方法を指定します。
"|"で区切って複数指定することが可能です。
指定値     デフォルト   動作     
top             コンテナの上部に配置し、サイズ変更は行いません。       
bottom          コンテナの下部に配置し、サイズ変更は行いません。       
left            コンテナの左側に配置し、サイズ変更は行いません。       
right           コンテナの右側に配置し、サイズ変更は行いません。       
center_vertical         上下中央に配置し、サイズ変更は行いません。  
fill_vertical           オブジェクトの高さを、コンテナのサイズに合わせます。     
center_horizontal               左右中央に配置し、サイズ変更は行いません。  
fill_horizontal         オブジェクトの幅を、コンテナのサイズに合わせます。      
center          上下左右中央に配置し、サイズ変更は行いません。
fill    ○       オブジェクトの高さ・幅を、コンテナのサイズに合わせます。   
clip_vertical           top/bottomの追加オプションとして、オブジェクトの上部/下部の境界をコンテナの境界に合わせます。   
clip_horizontal         left/rightの追加オプションとして、オブジェクトの左側/右側の境界をコンテナの境界に合わせます。   
 
 
 
  _____  
 
配置する向きを指定します。
指定値     デフォルト   動作     
horizontal      ○       水平方向に並べます。     
vertical                垂直方向に並べます。     
 
 

UIコンポーネント/FrameLayout

SUMMARY
 
継承するXML属性      
android.view.Viewの属性を継承します。    
android.view.ViewGroupの属性を継承します。       
 
XML属性  
属性      対応メソッド  概要     
android:foreground      setForeground(Drawable) このコンテナに描画するオブジェクトを指定します。       
android:foregroundGravity       setForegroundGravity(int)       オブジェクトの配置方法を指定します。     
android:measureAllChildren      setMeasureAllChildren(boolean)  子オブジェクトの描画領域を取得するかどうか指定します。    
 
 
XML属性
 
 
 
このコンテナに描画するオブジェクトを指定します。
Foregroundを指定することによって、このコンテナを
オーバレイとして使用することができます。
forgroundGravityに"fill"が指定されている場合、
オブジェクトはコンテナと連動してサイズ変更されます。
 
このタイプの値を含めている他のリソース、テーマ属性の参照を指定します。
または、"#rgb"、"#argb"、"#rrggbb"、"#aarrggbb"型で色を指定します。
 
 
  • サンプルコード
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
            android:id="@+id/TestSample"
            android:foreground="@color/FORE"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content">
            <Button
              android:id="@+id/continue_button"
              android:layout_width="fill_parent"
              android:layout_height="wrap_content"
              android:text="Button"/>
</FrameLayout>
 
結果
 
 
  _____  
 
オブジェクトの配置方法を指定します。
"|"で区切って複数指定することが可能です。
指定値     デフォルト   動作     
top             コンテナの上部に配置し、サイズ変更は行いません。       
bottom          コンテナの下部に配置し、サイズ変更は行いません。       
left            コンテナの左側に配置し、サイズ変更は行いません。       
right           コンテナの右側に配置し、サイズ変更は行いません。       
center_vertical         上下中央に配置し、サイズ変更は行いません。  
fill_vertical           オブジェクトの高さを、コンテナのサイズに合わせます。     
center_horizontal               左右中央に配置し、サイズ変更は行いません。  
fill_horizontal         オブジェクトの幅を、コンテナのサイズに合わせます。      
center          上下左右中央に配置し、サイズ変更は行いません。
fill    ○       オブジェクトの高さ・幅を、コンテナのサイズに合わせます。   
clip_vertical           top/bottomの追加オプションとして、オブジェクトの上部/下部の境界をコンテナの境界に合わせます。   
clip_horizontal         left/rightの追加オプションとして、オブジェクトの左側/右側の境界をコンテナの境界に合わせます。   
 
 
 
  • サンプルコード
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
            android:id="@+id/TestSample"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:foreground="@drawable/arrow_up_float"
            android:foregroundGravity="right">
            <Button
              android:foreground="@drawable/icon"
              android:id="@+id/continue_button"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:background="#FFFF0000"
              android:text="Button"/>
</FrameLayout>
 
結果
  _____  
 
表示/非表示に関わらず、子オブジェクトの描画領域を
取得するかどうか指定します。
指定値     デフォルト   動作     
true            表示/非表示に関わらず描画領域を計測します。 
false   ○       非表示の場合は描画領域を計測しません。    
 
 
 
 
 

FrameLayoutを使ってレイアウトを重ねる方法(位置指定もOK)

LinearLayoutで画面のオブジェクトを配置していくと、オブジェクト同士を重ねることはできない。
Androidの画面レイアウトは、Wordの文章に図を貼り付けた時のように、オブジェクト同士が干渉しないよう自動調整される。
これはこれで便利なのだが、ちょっとレイアウトを工夫しようと思ったらなかなか上手くいかない場合がある。
 
そこで、ビューを重ねて配置するのがFrameLayout。このレイアウトは最もシンプルなレイアウトと言われており、中のオブジェクトをとにかく左上に配置する。例えば、タグ内に3つのオブジェクトがあれば、3つとも左上に重ねて表示される。他のレイアウトとは異なり、位置を指定することもできない。それ故に、何のためのレイアウトか良く分からないという意見も。
 
私も当初、「使えないヤツ」と思っていたけど、そうでもないことが判明。FrameLayout内にLinearLayoutを配置すれば、その中のオブジェクトWidgitはちゃんと位置指定できる。
 
例えばこんな感じ。
 
 
<?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="fill_parent">
 
    <FrameLayout android:layout_width="fill_parent"
      android:id="@+id/frameLayout1"
      android:layout_height="wrap_content">
        <LinearLayout android:layout_width="fill_parent"
          android:id="@+id/linearLayout1"
          android:layout_height="fill_parent"
          android:orientation="vertical">
            <Button android:layout_width="fill_parent"
              android:id="@+id/button1"
              android:text="Button"
              android:layout_height="wrap_content">
            </Button>
            <Button android:layout_width="fill_parent"
              android:id="@+id/button2"
              android:text="Button"
              android:layout_height="wrap_content">
            </Button>
            <Button android:layout_width="fill_parent"
              android:id="@+id/button3"
              android:text="Button"
              android:layout_height="wrap_content">
            </Button>
        </LinearLayout>
        <LinearLayout android:layout_width="fill_parent"
          android:id="@+id/linearLayout2"
          android:layout_height="fill_parent"
          android:orientation="vertical">
            <TextView android:text="TextView"
              android:id="@+id/textView1"
              android:layout_marginLeft="100dp"
              android:layout_marginTop="70dp"
              android:textSize="30sp"
              android:textColor="#FFFF0000"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content">
            </TextView>
        </LinearLayout>
    </FrameLayout>
 
</LinearLayout>
 
これで、下のようにボタンとテキストを重ねて表示できる。
 
 

FrameLayout ウィジェットを重ね合わせて配置する。

FrameLayoutは、後に配置したウィジェットが先に配置したウィジェットの上に重ね合わせて表示される。

透過色を使って背景画像に上書きする形で、 下のレイヤーから上のレイヤーまで何階層かに渡って画像を合成して表示させるのに便利かな。

次の例は、FrameLayoutを使って大きさの異なるボタンを重ね合わせて表示する、xmlレイアウトファイルの例である。

リスト1(main.xml)

01 <?xml version="1.0" encoding="utf-8"?>
02 <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
03 android:layout_width="fill_parent"
04 android:layout_height="fill_parent"
05 >
06 <Button
07 android:id="@+id/button1"
08 android:layout_width="400px"
09 android:layout_height="400px"
10 android:text="1"
11 />
12 <Button
13 android:id="@+id/button2"
14 android:layout_width="200px"
15 android:layout_height="200px"
16 android:text="2"
17 />
18 <Button
19 android:id="@+id/button3"
20 android:layout_width="100px"
21 android:layout_height="100px"
22 android:text="3"
23 />
24 </FrameLayout>


FrameLayoutの内部に最初に配置したbutton1ボタンが最背面に、 2つ目に配置したbutton2ボタンがbutton1ボタンの前面に、 最後に配置したbutton3ボタンが更にbutton2ボタンの前面にと、 後に配置したウィジェットほど前面に表示されているのがわかる。


動的にウィジェットの重ね合わせの順番を変更する。


FrameLayoutの表示順は、先に追加されたウィジェットほど下層のレイヤーに表示されるので、 この追加順を変更してやる事で動的にウィジェットの重ね合わせの順番を変更する事ができる。

これには、ViewGroupクラスのremoveViewメソッドとaddViewメソッドを使う。

addViewメソッドは、レイアウトにウィジェットを追加する時におなじみのメソッドであるが、 追加位置を指定するオーバロードされたメソッドも存在する、このメソッドを使う。

表示順(位置)を変更したいウィジェットを、一旦、removeViewメソッドを使ってFrameLayoutから削除した後、 addViewメソッドを使って挿入位置を指定して、再度、削除したウィジェットを追加してやる。


以下は、リスト1のレイアウトファイルの3つのButtonに対して、 Buttonが押下された時に押下されたButtonを最背面に移動させるプログラムの例である。

リスト2(FreamLayut1Activity.java)

01 package gudon.sample.fream_layut1;
02
03 import android.app.Activity;
04 import android.os.Bundle;
05 import android.view.View;
06 import android.view.View.OnClickListener;
07 import android.view.ViewGroup;
08
09 public class FreamLayut1Activity extends Activity implements OnClickListener {
10 @Override
11 public void onCreate(Bundle savedInstanceState) {
12 super.onCreate(savedInstanceState);
13 setContentView(R.layout.main);
14
15 findViewById(R.id.button1).setOnClickListener(this);
16 findViewById(R.id.button2).setOnClickListener(this);
17 findViewById(R.id.button3).setOnClickListener(this);
18 }
19
20 @Override
21 public void onClick(View v) {
22 ViewGroup p=(ViewGroup)v.getParent();
23 p.removeView(v);
24 p.addView(v,0);
25 }
26 }

このプログラムを実行して、Buttonを押下すると、押下されたButtonが最下層(最背面)に移動する。


FrameLayoutとGravity

例えば、以下のようにリスト1を修正して、6行目にGravityの指定行を追加してもbuttonは下方に移動しない。

リスト3(main.xml)

2 <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 android:layout_width="fill_parent"
4 android:layout_height="fill_parent"
5 android:gravity="bottom"
6 >

しかし、Gravityがまったく使えないのかというと、そうでもない。

FrameLayout内部に配置したウィジェットに対して、個々にlayout_gravity属性を指定すればよい。

例えばリスト1を以下のように修正して、 button2を下方向に,button3を右方向にlayout_gravity属性を指定する。

リスト4(main.xml)

01 <?xml version="1.0" encoding="utf-8"?>
02 <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
03 android:layout_width="fill_parent"
04 android:layout_height="fill_parent"
05 >
06 <Button
07 android:id="@+id/button1"
08 android:layout_width="400px"
09 android:layout_height="400px"
10 android:text="1"
11 />
12 <Button
13 android:id="@+id/button2"
14 android:layout_width="200px"
15 android:layout_height="200px"
16 android:text="2"
17 android:layout_gravity="bottom"
18 />
19 <Button
20 android:id="@+id/button3"
21 android:layout_width="100px"
22 android:layout_height="100px"
23 android:text="3"
24 android:layout_gravity="right"
25 />
26 </FrameLayout>

このプログラムを実行すると、意図した方向にbuttonが移動しているのがわかる。

FrameLayoutとマージン
FrameLayoutの内部に配置するウィジェットにマージンを設定する事で、 表示位置が重ならないように位置をずらすことができる。

リスト4のレイアウトファイルを以下のように修正する。

リスト5(main.xml)

01 <?xml version="1.0" encoding="utf-8"?>
02 <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
03 android:layout_width="fill_parent"
04 android:layout_height="fill_parent"
05 >
06 <Button
07 android:id="@+id/button1"
08 android:layout_width="400px"
09 android:layout_height="400px"
10 android:text="1"
11 android:layout_gravity="left|top"
12 android:layout_marginLeft="50px"
13 android:layout_marginTop="50px"
14 />
15 <Button
16 android:id="@+id/button2"
17 android:layout_width="200px"
18 android:layout_height="200px"
19 android:text="2"
20 android:layout_gravity="bottom"
21 android:layout_marginLeft="50px"
22 android:layout_marginBottom="50px"
23 />
24 <Button
25 android:id="@+id/button3"
26 android:layout_width="100px"
27 android:layout_height="100px"
28 android:text="3"
29 android:layout_gravity="right"
30 android:layout_marginRight="50px"
31 android:layout_marginTop="50px"
32 />
33 </FrameLayout>

指定したマージンの値が、Buttonの位置に反映されているのがわかる。

ただし、注意すべき点として、FrameLayoutのマージンはlayout_gravityの指定が無いと有効にならないようだ。

layout_gravityがデフォルトではtop|leftで指定されているはずだからと思って、リスト5の11行目のlayout_gravityの指定を削除してしまうと、 button1はマージンで指定された位置に移動しなくなる。


foreground属性とforegroundGravity属性 - 最前面に更に画像を表示


foreground属性を指定して、子ウィジェットの更に前面(最前面)に画像を表示させる事ができる。

また、foregroundGravity属性を指定して、foreground属性に指定した画像のGravityを指定する事ができる。

勘違いしやすい点であるが、foregroundGravity属性は、foregroundに指定した画像に対するGravityの指定であって、 子ウィジェットに対するGravityの指定ではないようなので注意。

リスト5のレイアウトファイルのFrameLayoutの部分を、 以下のように修正して foreground属性とforegroundGravity属性を確認してみる。

リスト6(main.xml)

2 <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 android:layout_width="fill_parent"
4 android:layout_height="fill_parent"
5 android:foreground="@drawable/icon"
6 android:foregroundGravity="right|center_vertical"
7 >

foreground属性とforegroundGravity属性の指定により、アイコン画像が右側中央に表示されるのがわかる。

ちなみに、foregroundGravity属性はデフォルトでfillとなっており、指定しない場合、アイコン画像は画面一杯に拡大されて表示される。


FrameLayoutをコードで記述


リスト5,6のレイアウトを、レイアウトファイルを使わずにコードを使って記述すると、以下のようになる。

リスト7(FreamLayut2Activity.java)

01 package gudon.sample.fream_layut2;
02
03 import android.app.Activity;
04 import android.os.Bundle;
05 import android.view.Gravity;
06 import android.view.View;
07 import android.view.View.OnClickListener;
08 import android.view.ViewGroup;
09 import android.widget.Button;
10 import android.widget.FrameLayout;
11 import android.widget.FrameLayout.LayoutParams;
12
13 public class FreamLayut2Activity extends Activity implements OnClickListener {
14
15 @Override
16 public void onCreate(Bundle savedInstanceState) {
17 super.onCreate(savedInstanceState);
18
19 FrameLayout layout = new FrameLayout(this);
20 layout.setForeground(getResources().getDrawable(R.drawable.icon));
21 layout.setForegroundGravity(Gravity.RIGHT | Gravity.CENTER_VERTICAL);
22 setContentView(layout);
23
24 Button button1 = new Button(this);
25 button1.setText("1");
26 LayoutParams layoutParams1 = new LayoutParams(400, 400);
27 layoutParams1.gravity = Gravity.LEFT | Gravity.TOP;
28 layoutParams1.leftMargin = 50;
29 layoutParams1.topMargin = 50;
30 layout.addView(button1, layoutParams1);
31 button1.setOnClickListener(this);
32
33 Button button2 = new Button(this);
34 button2.setText("1");
35 LayoutParams layoutParams2 = new LayoutParams(200, 200);
36 layoutParams2.gravity = Gravity.BOTTOM;
37 layoutParams2.leftMargin = 50;
38 layoutParams2.bottomMargin = 50;
39 layout.addView(button2, layoutParams2);
40 button2.setOnClickListener(this);
41
42 Button button3 = new Button(this);
43 button3.setText("1");
44 LayoutParams layoutParams3 = new LayoutParams(100, 100);
45 layoutParams3.gravity = Gravity.RIGHT;
46 layoutParams3.rightMargin = 50;
47 layoutParams3.topMargin = 50;
48 layout.addView(button3, layoutParams3);
49 button2.setOnClickListener(this);
50 }
51
52 @Override
53 public void onClick(View v) {
54 ViewGroup p = (ViewGroup) v.getParent();
55 p.removeView(v);
56 p.addView(v, 0);
57 }
58 }

FrameLayoutのxml属性については、以下のページが参考になるかも。

* faviconUIコンポーネント/FrameLayout - Android Wiki* <http://wikiwiki.jp/android/?UI%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8%2FFrameLayout>
FrameLayoutのxml属性についてまとめられている。
* faviconUIコンポーネント/FrameLayout.LayoutParams - Android Wiki* <http://wikiwiki.jp/android/?UI%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8%2FFrameLayout.LayoutParams>
FrameLayoutクラスのxml属性についてまとめられている。

2012年11月5日月曜日

Android - AsyncTaskLoader

Loaderを使用する意図
今まで、HTTP通信などの時間がかかる処理を行う場合はAsyncTaskを用いて非同期処理を行い、SQLiteデータベースとのやり取りを行う場合はCursorを用いてstartManagingCursor()で Activityのライフサイクルに依存させるのが定番の実装であった。 が、AsyncTaskの場合はUIスレッドで行う処理を記述するonPreExecute()やonPostExecute()の実装がどうしてもActivityに依存してしまう為、 各Activityのインナークラスとして定義せざるを得なかった。その為、筆者は「元となる共通化されたAsyncTaskを定義し、それをextendsしたものを各Activity内でインナークラスとして定義する」 といったような実装をしていた。こうすると、各Activity内ではonPreExecute()とonPostExecute()を実装したクラスを定義すれば良くなる。だがこれはあくまでextendsしたものであるので、 親クラスの実装に依存する。

キャッシュを利用したい時はinitLoader()、最新を取得したい時はrestartLoader()を使い分けることによって、半永続的なキャッシュ処理が容易になる
非同期部分(共通化できる部分)はLoaderに定義し、各ActivityやFragmentではそれらに依存する処理をLoaderCallbacks<T>を implementsして定義することにより、MVCモデルにおけるモデルとコントローラの分離ができる
CursorLoaderに関しては、今までUIスレッドで行なっていたSQLiteとの通信処理を非同期で行う事により、 何度もクエリを投げなければならない場合などのまさかのANRの回避及びユーザビリティの向上に繋がる
という事だと思う。CursorLoaderを使用する場合はSQLiteとのやり取りがコンテンツプロバイダでwrapされていることが前提なので、行儀が良くなる事も長所かもしれない。 多分今までコンテンツプロバイダを利用する事はあっても、SQLiteを利用する為に自分で作成したという人はあまりいないだろう。 よくコンテンツプロバイダの説明で「他のアプリとのデータの共有(外部にデータを公開するインターフェースを定義する)」とか書いてあるが、その用途でアプリを作成している事は稀だからだ。 が、常にコンテンツプロバイダを介する事により、「実装の隠蔽(直接クエリを投げない)」という意味では行儀が良いのは間違いない。
ちなみに、AndroidManifest.xmlの定義でコンテンツプロバイダにしたとしてもそれをアプリ内部からしか見えないようにする事は可能なので安心。

まずは公式の翻訳を読もう。 大体の使い方は分かるが、もっと具体的な実装の流れを知りたいと思うだろう。その場合は以下の記事に付き合って頂きたい。

AsyncTaskLoaderの各メソッド
AsyncTaskの代わりになるのがこのAsyncTaskLoaderだ。しかしこのAsyncTaskLoaderのメソッドをどう使うべきなのかよくわからない。あとLoaderCallbacks<T>との兼ね合いもよく分からない。 ので、以下のようにログを埋め込んで試してみた。対象は、ドキュメントに「サブクラスでオーバーライドせよ」と書いてあるonXXX系のメソッドをフレームワークのソースを見ながら「何となく」選び、 あとLoaderCallbacks<T>のメソッドとする。各メソッドがどのタイミングで呼ばれ、どういう動きをするのか追ってみる。
以下、着目すべきはAsyncTaskLoaderの方には共通的な処理を書き、LoaderCallbacks<T>には各ActivityやFragmentに依存した処理を書く前提という事である。 例えば、通信を行うときにProgressDialogを出す→通信→ダイアログ閉じる、のような処理はどう挟み込むべきか、といった感じである。

注意すべきはAsyncTaskLoaderのonStartLoading()にforceLoad()を書いていることだ。これを書かないとロードが始まらない。
ここに書くのとLoaderCallbacksのonCreateLoader()に書く流儀があるようだが、Loaderの方は共通処理、Callbacksの方はビジネスロジック依存、
ということから考えるとLoaderに定義する方が勝ると思う。公式のCursorLoaderのソースもそうなっているし。

public class MainActivity extends FragmentActivity implements LoaderCallbacks<String>{
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
LoaderManager.enableDebugLogging(true);
findViewById(R.id.button1).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
getSupportLoaderManager().initLoader(0, null, MainActivity.this);
}
});
findViewById(R.id.button2).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
getSupportLoaderManager().restartLoader(0, null, MainActivity.this);
}
});
findViewById(R.id.button3).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
getSupportLoaderManager().destroyLoader(0);
}
});
findViewById(R.id.button4).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View arg0) {
Loader<Object> loader = getSupportLoaderManager().getLoader(0);
if (loader != null) {
AsyncTaskLoader<?> loader2 = (AsyncTaskLoader<?>)loader;
loader2.cancelLoad();
}
}
});
}

@Override
public Loader<String> onCreateLoader(int id, Bundle args) {
Log.d(getClass().getSimpleName(), "onCreateLoader.");
return new MyLoader(this);
}

@Override
public void onLoadFinished(Loader<String> loader, String result) {
Log.d(getClass().getSimpleName(), "onLoadFinished.");
}

@Override
public void onLoaderReset(Loader<String> loader) {
Log.d(getClass().getSimpleName(), "onLoaderReset.");
}

static class MyLoader extends AsyncTaskLoader<String> {
public MyLoader(Context context) {
super(context);
}

@Override
public String loadInBackground() {
Log.d(getClass().getSimpleName(), "loadInBackground start.");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.d(getClass().getSimpleName(), "loadInBackground end.");
return "";
}

@Override
protected void onAbandon() {
Log.d(getClass().getSimpleName(), "onAbandon.");
}

@Override
public void onCanceled(String data) {
Log.d(getClass().getSimpleName(), "onCanceled.");
}

@Override
protected void onReset() {
Log.d(getClass().getSimpleName(), "onReset.");
}

@Override
protected void onStartLoading() {
Log.d(getClass().getSimpleName(), "onStartLoading.");

// これを入れないとonStartLoadingで止まってしまう.
forceLoad();
}

@Override
protected void onStopLoading() {
Log.d(getClass().getSimpleName(), "onStopLoading.");
}
}
}
実行していないIDに対しinitLoader()

05-10 12:54:28.915: V/LoaderManager(7685): initLoader in LoaderManager{40554fc8 in MainActivity{4054ed20}}: args=null
05-10 12:54:28.915: D/MainActivity(7685): onCreateLoader.
05-10 12:54:28.915: V/LoaderManager(7685): Starting: LoaderInfo{405555a8 #0 : MyLoader{40555610}}
05-10 12:54:28.915: D/MyLoader(7685): onStartLoading.
05-10 12:54:28.915: V/LoaderManager(7685): Created new loader LoaderInfo{405555a8 #0 : MyLoader{40555610}}
05-10 12:54:28.915: D/MyLoader(7685): loadInBackground start.
05-10 12:54:31.918: D/MyLoader(7685): loadInBackground end.
05-10 12:54:31.918: V/LoaderManager(7685): onLoadComplete: LoaderInfo{405555a8 #0 : MyLoader{40555610}}
05-10 12:54:31.918: V/LoaderManager(7685): onLoadFinished in MyLoader{40555610 id=0}: String{40028418}
05-10 12:54:31.918: D/MainActivity(7685): onLoadFinished.
Callbacks.onCreateLoader() → Loader.onStartLoading() → Loader.loadInBackground → Callbacks.onLoadFinished().

既に実行したIDに対しinitLoader()

05-10 12:55:10.735: V/LoaderManager(7685): initLoader in LoaderManager{40554fc8 in MainActivity{4054ed20}}: args=null
05-10 12:55:10.735: V/LoaderManager(7685): Re-using existing loader LoaderInfo{405555a8 #0 : MyLoader{40555610}}
05-10 12:55:10.735: V/LoaderManager(7685): onLoadFinished in MyLoader{40555610 id=0}: String{40028418}
05-10 12:55:10.735: D/MainActivity(7685): onLoadFinished.
Callbacks.onLoadFinished()が呼ばれて終了する。既にデータは取得済なので、キャッシュが利用されているのがわかる。

実行していないIDに対しrestartLoader()

05-10 12:55:54.038: V/LoaderManager(7685): restartLoader in LoaderManager{40560158 in MainActivity{40559b90}}: args=null
05-10 12:55:54.038: D/MainActivity(7685): onCreateLoader.
05-10 12:55:54.048: V/LoaderManager(7685): Starting: LoaderInfo{40560788 #0 : MyLoader{405607f0}}
05-10 12:55:54.048: D/MyLoader(7685): onStartLoading.
05-10 12:55:54.068: D/MyLoader(7685): loadInBackground start.
05-10 12:55:57.071: D/MyLoader(7685): loadInBackground end.
05-10 12:55:57.071: V/LoaderManager(7685): onLoadComplete: LoaderInfo{40560788 #0 : MyLoader{405607f0}}
05-10 12:55:57.071: V/LoaderManager(7685): onLoadFinished in MyLoader{405607f0 id=0}: String{40028418}
05-10 12:55:57.071: D/MainActivity(7685): onLoadFinished.
Callbacks.onCreateLoader() → Loader.onStartLoading() → Loader.loadInBackground → Callbacks.onLoadFinished(). initLoader()を呼んだ時と同じ動作。

既に実行したIDに対しrestartLoader()

05-10 12:56:13.657: V/LoaderManager(7685): restartLoader in LoaderManager{40560158 in MainActivity{40559b90}}: args=null
05-10 12:56:13.657: V/LoaderManager(7685): Making last loader inactive: LoaderInfo{40560788 #0 : MyLoader{405607f0}}
05-10 12:56:13.657: D/MyLoader(7685): onAbandon.
05-10 12:56:13.657: D/MainActivity(7685): onCreateLoader.
05-10 12:56:13.657: V/LoaderManager(7685): Starting: LoaderInfo{40561f50 #0 : MyLoader{40561fb8}}
05-10 12:56:13.657: D/MyLoader(7685): onStartLoading.
05-10 12:56:13.657: D/MyLoader(7685): loadInBackground start.
05-10 12:56:16.670: D/MyLoader(7685): loadInBackground end.
05-10 12:56:16.670: V/LoaderManager(7685): onLoadComplete: LoaderInfo{40561f50 #0 : MyLoader{40561fb8}}
05-10 12:56:16.670: V/LoaderManager(7685): onLoadFinished in MyLoader{40561fb8 id=0}: String{40028418}
05-10 12:56:16.670: D/MainActivity(7685): onLoadFinished.
05-10 12:56:16.670: V/LoaderManager(7685): Destroying: LoaderInfo{40560788 #0 : MyLoader{405607f0}}
05-10 12:56:16.670: D/MyLoader(7685): onReset.
Loader.onAbandon() → Callbacks.onCreateLoader() → Loader.onStartLoading() → Loader.loadInBackground → Callbacks.onLoadFinished() → Loader.onReset().
古いローダの利用を止める→新しいローダ実行、取得→古いローダを終了、という流れのようだ。ログを見る限りCallbacks.onLoaderReset()は呼ばれない事に注意。

実行していないIDに対しdestroyLoader()

05-10 12:57:04.476: V/LoaderManager(7685): destroyLoader in LoaderManager{4056b978 in MainActivity{405654f0}} of 0
実行していないので、当然何も起こらない。

既に実行したIDに対しdestroyLoader()

05-10 12:57:38.249: V/LoaderManager(7685): destroyLoader in LoaderManager{4056b978 in MainActivity{405654f0}} of 0
05-10 12:57:38.249: V/LoaderManager(7685): Destroying: LoaderInfo{4056c848 #0 : MyLoader{4056c8b0}}
05-10 12:57:38.249: V/LoaderManager(7685): Reseting: LoaderInfo{4056c848 #0 : MyLoader{4056c8b0}}
05-10 12:57:38.249: D/MainActivity(7685): onLoaderReset.
05-10 12:57:38.249: D/MyLoader(7685): onReset.
Callbacks.onLoaderReset() → Loader.onReset(). 今度はCallbacks.onLoaderReset()が呼ばれた。再利用ではなく完全に破棄の場合に呼ばれるようだ。

Loader実行後に画面を閉じる(ActivityのonDestroy()発生)

05-10 13:00:54.601: V/LoaderManager(7685): Stopping in LoaderManager{4056b978 in MainActivity{405654f0}}
05-10 13:00:54.601: V/LoaderManager(7685): Stopping: LoaderInfo{4056ee90 #0 : MyLoader{4056eef8}}
05-10 13:00:54.601: D/MyLoader(7685): onStopLoading.
05-10 13:00:54.601: V/LoaderManager(7685): Destroying Active in LoaderManager{4056b978 in MainActivity{405654f0}}
05-10 13:00:54.601: V/LoaderManager(7685): Destroying: LoaderInfo{4056ee90 #0 : MyLoader{4056eef8}}
05-10 13:00:54.611: V/LoaderManager(7685): Reseting: LoaderInfo{4056ee90 #0 : MyLoader{4056eef8}}
05-10 13:00:54.611: D/MainActivity(7685): onLoaderReset.
05-10 13:00:54.611: D/MyLoader(7685): onReset.
05-10 13:00:54.611: V/LoaderManager(7685): Destroying Inactive in LoaderManager{4056b978 in MainActivity{405654f0}}
Loader.onStopLoading() → Callbacks.onLoaderReset() → Loader.onReset().
Loader.onStopLoading()が初めて出てきた。が、あまり使わないのかも。

loadInBackground実行中にLoader.cancelLoad()

05-10 13:27:26.012: V/LoaderManager(8516): initLoader in LoaderManager{40546be8 in MainActivity{4053f818}}: args=null
05-10 13:27:26.012: D/MainActivity(8516): onCreateLoader.
05-10 13:27:26.012: V/LoaderManager(8516): Starting: LoaderInfo{405471c8 #0 : MyLoader{40547230}}
05-10 13:27:26.012: D/MyLoader(8516): onStartLoading.
05-10 13:27:26.012: V/LoaderManager(8516): Created new loader LoaderInfo{405471c8 #0 : MyLoader{40547230}}
05-10 13:27:26.022: D/MyLoader(8516): loadInBackground start.
05-10 13:27:29.025: D/MyLoader(8516): loadInBackground end.
05-10 13:27:29.025: D/MyLoader(8516): onCanceled.
(一連のロード開始処理)→ Loader.loadInBackground → Loader.onCanceled.
当然ながらloadInBackgroundは最後まで実行されるのだが、最後まで実行されてからonCanceledが呼ばれるようだ。
ちなみにgetLoader()で対象IDをのローダが無い場合はnullが返るので、上記コードのようにnullチェックを行うこと。

ちなみに

ActivityのonPause()やonResume()のタイミングでも自動で何かやってくれるのかと思って試してみたが、何も起きなかった。
更に、initLoader()を連打した場合は、結局ロードは1回しか行われず、連打分だけ再利用されたが、Callbacks.onLoadFinished()は一度だけ呼ばれた。
restartLoader()を連打した場合は、その連打回数だけロードされるが、「直列に」行われた(前のローダが終了してから、次のローダが開始された)
一応書くが、すべて「同一IDのローダの場合」である。IDが違う場合は、それぞれ「並列に」ロードされた。

まとめると
LoaderCallbacks<T>

onCreateLoader() ローダ新規作成時(=新しいデータを取りに行く時)に呼ばれる。次に必ず長時間の非同期処理が走る。 ProgressDialogなどの経過処理を出すならここで出すべき。
onLoadFinished() 「新しい、古い関係なく」データ取得完了時に呼ばれる。ProgressDialogなどの経過処理を非表示にする処理をここに書くが、 「表示されていない場合もエラーにならない」ように書くべき。
onLoaderReset() 「ローダが完全に破棄」される場合に呼ばれる。「ローダ再利用(古いローダを破棄)」の場合は呼ばれない。リソース開放などの処理を書くべきか。
AsyncTaskLoaderの方のメソッドのオーバーライドは細かく動作を制御したい時のみオーバライドすれば良いかも。前述の通り、onStartLoading()にforceLoad()だけ書こう。
ロードのキャンセル処理も、結局UIの方でLoader.cancelLoad();を明示的に呼ぶので、そこで何かアクションをすればいいはず。 Loader.onCanceled()の方では、もし必要であれば、例えば既にキャッシュ用のリソースにデータを書き込んでしまった場合は、それを巻き戻すとか。(イマイチな例かも。そもそもloadInBackground内でキャッシュ処理書かないだろうし)

つまり、実践例としては以下のような雰囲気になるだろう。(cancelとdestroyは端折って最小構成にしてある)

public final class MainActivity extends FragmentActivity {

/** 個人的にはActivity / Fragmentにimplementsするよりこのようにprivateフィールドにした方が見通しが良くなる気がする. */
private final LoaderCallbacks<String> mLoaderCallbacks = new LoaderCallbacks<String>() {
@Override
public Loader<String> onCreateLoader(int id, Bundle bundle) {
// プログレスダイアログ表示.
ProgressDialogFragment dialog = new ProgressDialogFragment();
Bundle args = new Bundle();
args.putString("message", "データを読み込んでいます。");
dialog.setArguments(args);
dialog.show(getSupportFragmentManager(), "progress");
return new MyLoader(MainActivity.this);
}

@Override
public void onLoadFinished(Loader<String> loader, String result) {
// プログレスダイアログを安全に閉じる.
ProgressDialogFragment dialog = (ProgressDialogFragment)getSupportFragmentManager().findFragmentByTag("progress");
if (dialog != null) {
dialog.onDismiss(dialog.getDialog());
}

// TODO ここでUIへのデータ更新処理を行う.
}

@Override
public void onLoaderReset(Loader<String> loader) {
// TODO ここでデータの破棄の処理を行う. 基本書かなくていいかも.
}
};

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);

// LoaderManagerに関するデバッグログON. 動作を追うのに有用. 本番ビルド時は外そう.
LoaderManager.enableDebugLogging(true);

// リロードボタン. キャッシュを破棄し最新を取得する.
findViewById(R.id.reload).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
getSupportLoaderManager().restartLoader(0, null, mLoaderCallbacks);
}
});

// キャッシュがある場合はそれを使用しつつ, データを読み込む.
getSupportLoaderManager().initLoader(0, null, mLoaderCallbacks);
}

/** 今回のメインとなるAsyncTaskLoader. */
public static class MyLoader extends AsyncTaskLoader<String> {

/** コンストラクタ. */
public MyLoader(Context context) {
super(context);
}

/** 非同期で長い処理を行う. */
@Override
public String loadInBackground() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "";
}

/** 決まり文句のように書いて良い. 「こういう場合はinit/restartLoader呼ばれたけど読み込んでほしくない」という時はその条件を入れる. */
@Override
protected void onStartLoading() {
forceLoad();
}
}

/** プログレスダイアログ用のDialogFragment. 本来は別ファイルに書くが. */
public static class ProgressDialogFragment extends DialogFragment {
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
ProgressDialog dialog = new ProgressDialog(getActivity());
dialog.setMessage(getArguments().getString("message"));
return dialog;
}
}
}
ちなみに
よくLoaderのメソッドであるdeliverResult()やその他onXXX系をオーバーライドして使用しているソースを見かけるが、 恐らくCursorLoaderの実装を見て真似ているのだと思うのだが、ContentProviderを絡めずにただ単にAsyncTaskの代替手段として用いるのであれば、 特にその他のメソッドをオーバーライドする必要はない。Loaderのソースを読めばわかるが、takeContentChanged()もContentProvider前提なので気にする必要はない。 また、CursorLoaderがフィールドにCursorの参照を持っているのは自前でCursor.close()の管理をしたいだけなので、AsyncTaskLoaderでこれの真似をする必要はない。 フィールドに持たなくても既に実行されたidに対しinitLoader()すれば前回の結果が返るので心配はない。 故に上記ソースのMyLoaderが最小構成である。

@Override
protected void onStopLoading() {
cancelLoad();
}
しかしこれは入れたほうがいいかもしれないと思った。上記調査から、onStopLoading()はLoader実行後に画面を閉じる(ActivityのonDestroy()発生)の時の呼ばれるので、 cancelLoad()しておけばとりあえずLoaderCallbacks.onLoadFinished()は呼ばれない。

Androidアプリのバグ報告システムを考える

MacとかWindowsでアプリが予期せぬ不具合で強制終了した後におもむろに出てくる「バグ報告」。
あれがSimejiにも欲しい。と思って作ってみたら予想以上に役に立ったので、ここでそのシステムを公開します。
アプリ開発者は絶対入れた方がいいですよ。ユーザの協力が得られればアプリの安定性向上に役に立つ事間違いなしです。

動作概要
catchしなくてよい例外
JavaにはNullPointerExceptionなどのcatchしなくてもclass load validationを素通りできる例外があります。
バグの多くはそういった例外を考慮しないことのようです。
なので、今回はそういった例外の「IndexOutOfBoundsException」を発生させます。

ボタンをタップすると例外が発生します。

oobBtn.setOnClickListener(new View.OnClickListener(){
public void onClick(View v) {
int index = 5;
String[] strs = new String[index];
String str = strs[index];//ここでIndexOutOfBoundsException
}});
IndexOutOfBoundsExceptionも例外処理を記述しなくてもコンパイルエラーにならない例外です。
この仕組みのおかげでプログラムが書きやすくなっていますが、バグも入りやすいので要注意です。

例外が発生すると
ボタンをタップすると例外が発生するのですがcatchされずにアプリが強制終了します。

どこでこの例外が発生したのかが分かると、多くの問題は解決可能です。
これを捕捉するシステムを考えるのが本エントリの目的です。

catchしない例外を補足する
Javaにはcatchしなかった例外を補足するThread.UncaughtExceptionHandlerという仕組みが元々備わっています。
今回はコレを利用します。

public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Context context = getApplicationContext();
Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler(context));
}
*アプリにつき1回だけハンドラを登録するだけでOKです。スレッドは関係ありません。
MyUncaughtExceptionHandlerの引数にActivityを渡すとメモリリークする可能性があります。
onCreateメソッドの中でUncaughtExceptionHandlerを登録しておきます。
このハンドラは独自に拡張したMyUncaughtExceptionHandlerです。
どこにもcatchされなかった例外は、最終的にこのハンドラに渡されますので、
捕捉できなかった例外をハンドラ内で処理します。

public class MyUncaughtExceptionHandler implements UncaughtExceptionHandler {
@Override
public void uncaughtException(Thread th, Throwable e) {
//catchされなかった例外は最終的にココに渡される
}
}
catchされなかった例外は、最終的にuncaughtExceptionメソッドの引数に渡されコールバックされます。
そのため、このメソッド内にバグ報告の処理を書きます。
引数のThrowableから、例外発生点のスタックトレースを取り出します。

StackTraceElement[] stacks = e.getStackTrace();
開発者にとって、例外発生経路と発生点はバグ修正するにあたり、とても役立つ情報です。
この情報(バグ情報)をサーバなどに送信し、開発者に通知します。
以上がAndroidにおけるバグ報告システムの概要でした。とても簡単です。

続いて、Simejiで使っている具体的なシステムをご紹介します。
あくまで一例なので、他に良い方法があるかもしれません。

Simejiのバグ報告システム
設計
catchしていない例外が発生した場合、そのスタックトレースを外部記憶装置(SDカード等)に保存
アプリ起動時にSDカード内にバグ情報ファイルが存在する場合は、その内容を送信
設計方針の理由は単純です。なんらかの理由でアプリが終了しようとしているので、
スタックトレースなどの情報をアプリ内部に持たせるのは不可能(もしくは崩れるかもしれない)なのでSDカードに保存します。
また、同じ理由で、次の起動時にファイルをチェックし、ファイルが存在すればバグ情報を送信するようにしました。

スタックトレースをSDカードに保存する
まずは、catchされなかった例外が発生した時に、そのスタックトレースをSDカードに保存します。

public void uncaughtException(Thread th, Throwable t) {
//catchされなかった例外処理
try {
saveState(t);//ここでスタックトレースを保存
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}

private void saveState(Throwable e) throws FileNotFoundException {
StackTraceElement[] stacks = e.getStackTrace();//スタックトレース
File file = BUG_REPORT_FILE;//保存先
PrintWriter pw = null;
pw = new PrintWriter(new FileOutputStream(file));
StringBuilder sb = new StringBuilder();
int len = stacks.length;
for (int i = 0; i < len; i++) {
StackTraceElement stack = stacks[i];
sb.setLength(0);
sb.append(stack.getClassName()).append("#");//クラス名
sb.append(stack.getMethodName()).append(":");//メソッド名
sb.append(stack.getLineNumber());//行番号
pw.println(sb.toString());//ファイル書出し
}
pw.close();
}
例外発生時のスタックトレースはBUG_REPORT_FILEに保存しています。
BUG_REPORT_FILEの中身は以下の通りです。

private static File BUG_REPORT_FILE = null;
static {
String sdcard = Environment.getExternalStorageDirectory().getPath();
String path = sdcard + File.separator + "bug.txt";
BUG_REPORT_FILE = new File(path);
}
「/sdcard/bug.txt」というファイルに書出しています。
外部ストレージが/sdcardにマウントされるかは実装次第なので、ちょっと煩わしいですがEnvironmentを経由させておきます。

これで、catchされなかった例外が発生したスタックトレースを保存できました。
次に、アプリ起動時に、この情報をサーバに報告する部分を説明します。

バグ情報を送信する
アプリ起動時に、先ほどのバグ情報ファイルが存在するかをチェックし、
存在するならその内容をサーバに送信し、存在しないなら無視してアプリを起動します。

public void onStart(){
super.onStart();
//前回バグで強制終了した場合はダイアログ表示
MyUncaughtExceptionHandler.showBugReportDialogIfExist();
}

public static final void showBugReportDialogIfExist() {
File file = BUG_REPORT_FILE;
if (file != null & file.exists()) {
AlertDialog.Builder builder = new AlertDialog.Builder(sContext);
builder.setTitle("バグレポート");
builder.setMessage("バグ発生状況を開発者に送信しますか?");
builder.setNegativeButton("Cancel", new OnClickListener(){
public void onClick(DialogInterface dialog, int which) {
finish(dialog);//ダイアログの消去とファイルの削除
}});
builder.setPositiveButton("Post", new OnClickListener(){
public void onClick(DialogInterface dialog, int which) {
postBugReportInBackground();//バグ報告(別スレッドでファイルを削除)
dialog.dismiss();
}});
AlertDialog dialog = builder.create();
dialog.show();
}
}
*SDカード内のファイル操作をマルチスレッドを無視した実装になっていましたので修正しました。
ユーザの許可無くサーバに情報を送信するのはevilに感じるので、
ダイアログを表示し、送信の許可をユーザに求めます。

Postボタンがそれです。
PostボタンがタップされるとpostBugReportInBackgroundメソッドでバグ情報をサーバに送信します。

private static void postBugReportInBackground() {
new Thread(new Runnable(){
public void run() {
postBugReport();
BUG_REPORT_FILE.delete();
}}).start();
}

private static void postBugReport() {
List<NameValuePair> nvps = new ArrayList<NameValuePair>();
String bug = getFileBody(BUG_REPORT_FILE);
nvps.add(new BasicNameValuePair("dev", Build.DEVICE));
nvps.add(new BasicNameValuePair("mod", Build.MODEL));
nvps.add(new BasicNameValuePair("sdk", Build.VERSION.SDK));
nvps.add(new BasicNameValuePair("ver", sPackInfo.versionName));
nvps.add(new BasicNameValuePair("bug", bug));
try {
HttpPost httpPost = new HttpPost("http://foo.bar.org/bug");
httpPost.setEntity(new UrlEncodedFormEntity(nvps, HTTP.UTF_8));
DefaultHttpClient httpClient = new DefaultHttpClient();
httpClient.execute(httpPost);
} catch (IOException e) {
e.printStackTrace();
}
}
HTTP通信は、ある程度の処理時間がかかりますので、別スレッドで実行するようにしています。
見た目上のレスポンス性能は重要です。
getFileBodyメソッドはBUG_REPORT_FILEの中身をStringで取得しています。
スタックトレースの他に、デバイス名やSDKのバージョンなど、
バグが発生した個体情報も追加しておきます。
特に、アプリのバージョン番号「sPackInfo.versionName」は重要なので追加しておいた方が良いです。

sPackInfo変数は以下のようにして取得しています。

public MyUncaughtExceptionHandler(Context context) {
try {
//パッケージ情報
sPackInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
} catch (NameNotFoundException e) {
e.printStackTrace();
}
また、バグ報告の送信の有無に関わらず、BUG_REPORT_FILEを消しておきます。
このファイルの存在がトリガーになってバグ報告ダイアログが表示されるので、
一度表示した後はファイルを消しておきます。

private static void finish(DialogInterface dialog) {
File file = BUG_REPORT_FILE;
if (file.exists()) {
file.delete();
}
dialog.dismiss();
}
以上でhttp://foo.bar.org/bugにPOSTリクエストでバグ情報の送信が完了しました。
次にサーバ側です。
Google App Engine(GAE)を使って、バグ情報を格納し、開発者へメール連絡する仕組みをご紹介します。

GAEでバグレポート
データを格納してメールする程度の簡単な処理なので、
GAEは、記述量の少ないPythonで実装します。
データ構造は送信される内容のままです。

class BugData(db.Model):
device = db.StringProperty()#device name
model = db.StringProperty()#model name
sdk = db.StringProperty()#sdk name
version = db.StringProperty()#version number
bug = db.TextProperty()#stacktrace
create = db.DateTimeProperty(auto_now_add=True)
あとは、POSTリクエストを受けて、このデータベースに登録するだけです。

class BugReportHandler(webapp.RequestHandler):

def get(self):
self.get_or_post()

def post(self):
self.get_or_post()

def get_or_post(self):
dev = self.request.get("dev")
mod = self.request.get("mod")
sdk = self.request.get("sdk")
ver = self.request.get("ver")
bug = self.request.get("bug")

#insert a new element
db = BugData(device=dev, model=mod, sdk=sdk, version=ver, bug=bug)#row
db.put()#save

#report with email
mail.send_mail(sender="dev@gmail.com", to="dev@gmail.com", subject="Bug Report", body=bug)

self.response.out.write('Success!')
HTTPリクエストBODYから各要素(devやbugなど)を取り出し、
db.putメソッドでデータベースに登録しています。

スタックトレースの内容をmail.send_mailで開発者にメール送信しています。
これにより、開発者はブラウザでチェックしなくてもメールでバグの発生を知ることができます。
より利便性を高めるには、同じバグ報告をメールしないようにフィルタリング処理を追加するなどが考えられます。

以上が、AndroidアプリとGAEを使ったバグ報告システムでした。
バグの無いアプリが一番良いのですが、バグの無いプログラムは無いとも言われます。
バグとうまく付き合っていく方法として本システムはとても役立っていますので、
開発者の皆さんは参考にして頂き、ご自身のアプリを育てていって下さい。

おわりに
去年(2009年)の忘年会から、このネタをBlogにアップして欲しいと言われ続けて、今頃ようやく公開しました。
遅くなってスミマセン。これからも、こういった開発コネタをアップし、開発者のサポートができればと思います。
また、デ部でも、code snippetなどのノウハウを溜める仕組みができたらイイなぁ…
小さなソースコードを溜めていけるWebシステム(CMS?)を構築できる技術者を絶賛募集中。手伝って下さい><

Androidソースコード
Androidアプリ部分のソースコードを公開しておきます。
自由にご利用下さい。
BugReport.zip

内容は下記の通りです。

BugReportActivity.java
package com.adamrocker.android.bugreport;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

public class BugReportActivity extends Activity {

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Context context = getApplicationContext();
Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler(context));
setContentView(R.layout.main);
Button oobBtn = (Button)findViewById(R.id.oob_btn);
oobBtn.setOnClickListener(new View.OnClickListener(){
public void onClick(View v) {
int index = 5;
String[] strs = new String[index];
String str = strs[index];//ここでIndexOutOfBoundsException
}});
}

public void onStart(){
super.onStart();
//前回バグで強制終了した場合はダイアログ表示
MyUncaughtExceptionHandler.showBugReportDialogIfExist();
}
}
MyUncaughtExceptionHandler.java
package com.adamrocker.android.bugreport;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.Thread.UncaughtExceptionHandler;
import java.util.ArrayList;
import java.util.List;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.HTTP;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Build;
import android.os.Environment;

public class MyUncaughtExceptionHandler implements UncaughtExceptionHandler {
private static File BUG_REPORT_FILE = null;
static {
String sdcard = Environment.getExternalStorageDirectory().getPath();
String path = sdcard + File.separator + "bug.txt";
BUG_REPORT_FILE = new File(path);
}

private static Context sContext;
private static PackageInfo sPackInfo;
private UncaughtExceptionHandler mDefaultUEH;
public MyUncaughtExceptionHandler(Context context) {
sContext = context;
try {
//パッケージ情報
sPackInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
} catch (NameNotFoundException e) {
e.printStackTrace();
}
mDefaultUEH = Thread.getDefaultUncaughtExceptionHandler();
}

public void uncaughtException(Thread th, Throwable t) {
try {
saveState(t);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
mDefaultUEH.uncaughtException(th, t);
}

private void saveState(Throwable e) throws FileNotFoundException {
StackTraceElement[] stacks = e.getStackTrace();
File file = BUG_REPORT_FILE;
PrintWriter pw = null;
pw = new PrintWriter(new FileOutputStream(file));
StringBuilder sb = new StringBuilder();
int len = stacks.length;
for (int i = 0; i < len; i++) {
StackTraceElement stack = stacks[i];
sb.setLength(0);
sb.append(stack.getClassName()).append("#");
sb.append(stack.getMethodName()).append(":");
sb.append(stack.getLineNumber());
pw.println(sb.toString());
}
pw.close();
}

public static final void showBugReportDialogIfExist() {
File file = BUG_REPORT_FILE;
if (file != null & file.exists()) {
AlertDialog.Builder builder = new AlertDialog.Builder(sContext);
builder.setTitle("バグレポート");
builder.setMessage("バグ発生状況を開発者に送信しますか?");
builder.setNegativeButton("Cancel", new OnClickListener(){
public void onClick(DialogInterface dialog, int which) {
finish(dialog);
}});
builder.setPositiveButton("Post", new OnClickListener(){
public void onClick(DialogInterface dialog, int which) {
postBugReportInBackground();//バグ報告
dialog.dismiss();
}});
AlertDialog dialog = builder.create();
dialog.show();
}
}

private static void postBugReportInBackground() {
new Thread(new Runnable(){
public void run() {
postBugReport();
File file = BUG_REPORT_FILE;
if (file != null && file.exists()) [
file.delete();
}
}}).start();
}

private static void postBugReport() {
List<NameValuePair> nvps = new ArrayList<NameValuePair>();
String bug = getFileBody(BUG_REPORT_FILE);
nvps.add(new BasicNameValuePair("dev", Build.DEVICE));
nvps.add(new BasicNameValuePair("mod", Build.MODEL));
nvps.add(new BasicNameValuePair("sdk", Build.VERSION.SDK));
nvps.add(new BasicNameValuePair("ver", sPackInfo.versionName));
nvps.add(new BasicNameValuePair("bug", bug));
try {
HttpPost httpPost = new HttpPost("http://foo.bar.org/bug");
httpPost.setEntity(new UrlEncodedFormEntity(nvps, HTTP.UTF_8));
DefaultHttpClient httpClient = new DefaultHttpClient();
httpClient.execute(httpPost);
} catch (IOException e) {
e.printStackTrace();
}
}

private static String getFileBody(File file) {
StringBuilder sb = new StringBuilder();
try {
BufferedReader br = new BufferedReader(new FileReader(file));
String line;
while((line = br.readLine()) != null) {
sb.append(line).append("\r\n");
}
} catch (Exception e) {
e.printStackTrace();
}
return sb.toString();
}

private static void finish(DialogInterface dialog) {
File file = BUG_REPORT_FILE;
if (file.exists()) {
file.delete();
}
dialog.dismiss();
}
}
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.adamrocker.android.bugreport"
android:versionCode="1"
android:versionName="1.0">
<application android:icon="@drawable/icon" android:label="@string/app_name" android:debuggable="false">
<activity android:name=".BugReportActivity"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

</application>
<uses-sdk android:minSdkVersion="3" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission>
<uses-permission android:name="android.permission.INTERNET"></uses-permission>
</manifest>
GAEソースコード
GAEのソースコードは下記の通りです。

app.yaml
application: bug-store
version: 1
runtime: python
api_version: 1

handlers:
- url: /bug
script: bugs.py


bugs.py
from google.appengine.ext import db, webapp
from google.appengine.ext.webapp import util
from google.appengine.api import mail

class BugData(db.Model):
device = db.StringProperty()#device name
model = db.StringProperty()#model name
sdk = db.StringProperty()#sdk name
version = db.StringProperty()#version number
bug = db.TextProperty()#stacktrace
create = db.DateTimeProperty(auto_now_add=True)

class BugReportHandler(webapp.RequestHandler):

def get(self):
self.get_or_post()

def post(self):
self.get_or_post()

def get_or_post(self):
dev = self.request.get("dev")
mod = self.request.get("mod")
sdk = self.request.get("sdk")
ver = self.request.get("ver")
bug = self.request.get("bug")

#insert new element
db = BugData(device=dev, model=mod, sdk=sdk, version=ver, bug=bug)
db.put()

#report with email
mail.send_mail(sender="developer@gmail.com", to="developer@gmail.com", subject="Bug Report", body=bug)

self.response.out.write('Success!')

def main():
application = webapp.WSGIApplication([('/bug', BugReportHandler)],
debug=False)
util.run_wsgi_app(application)


if __name__ == '__main__':
main()