Software/Android/アプリケーション開発テキスト

Chapter 2 : 実用アプリケーション開発の基礎

 この章ではファイル検索アプリケーションの開発を通して、実用的なAndroidアプリケーション開発の基礎を解説します。

 Androidにはバックグラウンドで動作するサービス、ホーム画面に設置可能なウィジェットといった様々な形態のアプリケーションが存在しますが、本章では全画面を占有する画面(ウィンドウ)を持つ、一般的なアプリケーションを取り扱います。

2-1. アプリケーションプロジェクトの構成要素

 まずはベースとなるプロジェクトを用意します。以下の様に設定し、新規Androidアプリケーションプロジェクトを作成します。

  • Project Nameに「FFind」と入力します
  • Application Nameに「FFind」と入力します
  • Package Nameに「com.beatcraft.ffind」と入力します
  • Create Activityにチェックを入れ、「FFindActivity」と入力します 

 Androidアプリケーションのプロジェクトは大きく分けて以下の3つの要素で構成されます。

 .泪縫侫Д好肇侫.ぅ
  Javaソースコード
 リソース
02_01.png
図2-1


 作成したプロジェクトをPackage Explorerで確認しつつ、各要素について解説していきます。

.泪縫侫Д好肇侫.ぅ襦AndroidManifest.xml)
 このアプリケーションの名前やバージョン、どのような画面を持つか、どのような機能を持つかといった内容を記述したXMLファイルで、Package Explorerのプロジェクト直下に配置されています。Androidはこのマニフェストに記述された内容を元にアプリケーションを管理します。

 リスト2-1が今回自動的に作成されたマニフェストファイルです。

  • リスト2-1
    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.beatcraft.ffind"
        android:versionCode="1"
        android:versionName="1.0" >
    
        <uses-sdk android:minSdkVersion="8" />
    
        <application
            android:icon="@drawable/ic_launcher"
            android:label="@string/app_name" >
            <activity
                android:label="@string/app_name"
                android:name=".FFindActivity" >
                <intent-filter >
                    <action android:name="android.intent.action.MAIN" />
    
                    <category android:name="android.intent.category.LAUNCHER" />
                </intent-filter>
            </activity>
        </application>
    
    </manifest>

 application要素のandroid:icon属性がこのアプリケーションで使用するランチャーアイコンを示し、android:label属性はこのアプリケーションの名前を示します。
 それぞれの属性の内容は、後述するリソースを 「drawable/ic_launcher」「@string/app_name」といったIDで指定しています。

 activity要素にはこのアプリケーションに含まれるアクティビティに関する情報を記述します。アクティビティはアプリケーションに視覚的なユーザーインターフェースを提供するもので、他のOSで言う「画面」や「ウィンドウ」に相当します。通常は1つのアクティビティが全画面を占有し、複数のアクティビティを持つアプリケーションではこれを切り替えて使用します。
 activity要素内にあるintent-filter要素には、アクティビティが取り扱い可能なインテントについての情報を記述します。インテントはアクティビティや他のアプリケーション、バックグラウンドサービスなどとのメッセージ送受信に使用するオブジェクトで、画面遷移やアプリケーション間の連携に使用します。

 このマニフェストでは、FFindActivityアクティビティがこのアプリケーションでメインとなる画面であり、Androidのアプリケーションランチャーに登録され起動可能であることが示されています。

Javaソースコード
 このアプリケーションを構成するソースコードで、Package Explorerのsrc下に配置されます。現段階ではプロジェクト作成時に自動生成されたFFindActivity.javaがパッケージ下に置かれています。

  • リスト2-2 FFindActivity.java : 
    package com.beatcraft.ffind;
    
    import android.app.Activity;
    import android.os.Bundle;
    
    public class FFindActivity extends Activity {
        /** Called when the activity is first created. */
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.main);
        }
    }

 FFindActivity.java(リスト2-2)のonCreateメソッドは、FFindActivityアクティビティが生成される際に呼び出されるメソッドで、このアクティビティに必要な初期設定をここに記述します。ここではsuperクラスのonCreateを呼び出して基本的な初期設定を行なっているほか、setContentViewメソッドでこのアクティビティの外観を設定しています。
 setContentViewメソッドの引数に渡している「R.layout.main」というのは、後述するリソースから画面レイアウトを指定しています。

リソース
 リソースにはアプリケーションで使用される文字列や画像、音声、画面レイアウトといった様々な物が含まれます。Package Explorerではres下に配置され、リソースの種別毎に分類されています。プロジェクト作成時点では、drawable/layout/valuesという三種のリソースフォルダが作成されています。

 drawableには画像ファイルなどの描画可能なリソースが置かれます。-hdpi/-mdpi/-ldpiというサフィックスが付与された3つのフォルダが作成されていますが、これはAndroidアプリケーションを多様な環境に動的に対応させるための仕組みで、それぞれ高解像度/中解像度/低解像度端末用の描画リソースを配置します。
 どの環境でも同じ物を使用する場合は、サフィックスを付けないdrawableフォルダを作成し、そこに配置することも可能です。対応する解像度用フォルダにリソースが無い場合は他の解像度用リソースを自動的にスケールして使用されます。
 マニフェストのandroid:icon属性で指定した「@drawable/ic_launcher」が示す画像リソースの実体(ic_launcher.png)はここに含まれます。

 layoutにはアクティビティの画面レイアウトを記述したXMLファイルが置かれます。プロジェクト作成時点ではmain.xmlというレイアウトファイルが自動生成されています。
 FFindActivity.javaのsetContentViewメソッドで指定した「R.layout.main」がこのレイアウトを示しています。

 valuesには文字列や列挙といった、様々な値を定義したXMLファイルが置かれます。プロジェクト作成時点ではstrings.xmlという文字列を定義したXMLファイルが自動生成されています。フォルダに「-ja」や「-en」といったサフィックスをつけたものを作成することにより、言語別の値リソースを用意することも可能です。
 マニフェストのandroid:label属性で指定した「@string/app_name」が示す文字列リソースの内容がこのstrings.xmlに定義されています。

 リソースはリソースIDによってマニフェストやJavaソースコード、他のリソースから参照されます。リソースIDにはXMLファイルで参照される場合とJavaソースコードで参照される場合とで二種類の記法があり、以下のようになっています。

  • XMLファイルから参照する場合 :
    • @リソース種別/リソース名
  • Javaソースコードから参照する場合 :
    • R.リソース種別.リソース名

 drawableとlayoutはそのままリソース種別となり、リソース名にはリソース実体のファイル名からサフィックスを取り除いた物になります。これまで見た例で言えば、drawable-hdpi/mdpi/ldpiに含まれるic_launcher.pngのリソースIDは「@drawable/ic_launcher」「R.drawable.ic_launcher」、layoutに含まれるmain.xmlが「R.layout.main」になる、と言った具合です。

 valuesに含まれるXMLファイルのリソース種別はvaluesとはならず、それぞれのXMLファイル内で定義された要素名になります。自動生成されたstrings.xml(リスト2-3)には、resources要素に囲まれたstringという要素が並んでいます。この要素名がリソース種別となり、name属性がリソース名となります。
 マニフェストで指定した「@string/app_name」がリソースIDとなり、それが指し示す文字列が「FFind」と言った具合です。

  • リスト2-3 strings.xml :
    < xml version="1.0" encoding="utf-8" >
    <resources>
    
        <string name="hello">Hello World, FFindActivity!</string>
        <string name="app_name">FFind</string>
    
    </resources>


2-2. アクティビティの構成要素とレイアウト

 Androidアプリケーションの顔となるアクティビティは、アプリケーション画面の表示の枠組みを提供しますが、これだけでは何かを表示したりユーザー入力を受け付けたりといったことが出来ません。この枠の中に、ビュー(View)やビューグループ(ViewGroup)といった階層構造を持つGUI部品を設定する必要があります。

 ビューはGUI部品の基本となるもので、ボタンやテキストボックスなどは全てビューです。また後述のビューグループもビューの一種となります。ビューグループはビューを複数まとめて管理するもので、GUI部品をどのように配置するかを決定するレイアウトもこれに含まれます。

 レイアウトはGUI部品をどのように配置するかを決定づけるビューグループで、LinearLayoutやRelativeLayout、TableLayoutといった配置方法の異なるレイアウトが用意されています。

  • LinearLayout
    • LinearLayoutはビューを登録した順番に並べる標準的なレイアウトです。縦方向に並べていくか、横方向に並べていくかを選択出来ます。
  • RelativeLayout
    • RelativeLayoutはビュー同士の配置に関連性をもたせ、それぞれのビューの位置関係を指定することで複雑なレイアウトを実現します。
  • TableLayout
    • TableLayoutは表形式にビューを配置するレイアウトです。

 この他にも用途に応じたいくつかのレイアウトが提供されており、レイアウト自体も階層的に配置することが可能です。

2-3. アプリケーションのレイアウト作成

 ここからは作成したプロジェクトをもとに実際のアプリケーションに組み替えていきます。今回作成するアプリケーションはAndroid端末内のファイルを走査し、入力されたキーワードをファイル名に含むファイルを一覧表示するというものです。

 図2-2が今回のアプリケーションの画面イメージとなります。

02_02.png
図2-2


 EclipseのPackage Explorerからmain.xmlをダブルクリックして開き、上記イメージにあわせレイアウトを変更していきます。自動生成されたmain.xmlは図2-3のように、縦方向のLinearLayoutを再上位に置き、そこにTextViewを1つ配置した物になっています。

02_03.png
図2-3


 サンプルプロジェクトでは「Hello World, FFindActivity!」と表示するTextViewが配置されていますが、これは不要となるので中央のレイアウタで該当箇所をクリックするか、右部のOutlineから「TextView」となっている行をクリックして選択し、削除キーや右クリックによるポップアップメニューの「Delete」を選択して削除します。

 続いて左部のPaletteからLayoutsをクリックして展開し、LinearLayout (Horizontal)を先ほどTextViewがあった場所にドラッグ&ドロップで配置します。これでビューを横方向に並べるよう予め設定されたLinearLayoutが最上位のLinearLayout配下に追加されます。

 次にこのレイアウトにキーワードを入力するテキストボックスと検索ボタンを配置します。左部のPaltetteからText Fieldsをクリックして展開し、「abc」と書かれたテキストボックス(Plain Text)を今追加したレイアウト上に配置します。配置したテキストボックスをクリックし、レイアウタ下部の「Properties」(※)で以下のように設定を変更します。
※Propertiesが表示されていない場合、Eclipseのメニューから「Window > Show View > Other...」を選択して"Show View"ダイアログを表示し、「General > Properties」を選択して「OK」ボタンをクリックして下さい。

  • Idに「@+id/FindTarget」と入力します。
  • Layout heightに「wrap_content」を指定します。
  • Layout weightに「1」と入力します。
  • Layout widthに「wrap_content」を指定します。
  • Single lineに「true」を指定します。

 Idに入力した内容はこのテキストボックスのリソースIDになりますが、この時点でどこにも定義されていません。「@+リソース種別/リソース名」という記法は、この場で指定したリソース種別にリソース名を追加することを表します。

 Layout heightとLayout widthはこのビューの高さと幅を決定する属性で、wrap_contentを指定するとそのビュー自身が必要とするサイズに自動的に調整されます。この他、親となるビューのサイズにフィットさせるfill_parent(API level 8以降ではmatch_parent)や、サイズを直接指定することが可能です。
 Layout weightは親ビュー内で使用可能な領域をどの程度の重みで占有するかを決定する属性で、1を指定した場合、他のビューが配置された残りの領域全てを使用します。

 Single lineをtrueにすると、このテキストボックスには複数行の入力が出来なくなります。ファイル名の一部が改行を含む複数行にまたがることは有り得ないため、ここではtrueを設定しています。

 引き続き検索ボタンを追加します。左部のPaletteからForm Widgetsをクリックして展開し、「Button」をドラッグ&ドロップでテキストボックスの右側に配置します。配置したボタンをクリックし、レイアウタ下部の「Properties」で以下のように設定を変更します。

  • Idに「@+id/FindButton」と入力します。
  • Textに「@string/find_button」と入力します。
  • Layout heightに「wrap_content」を指定します。
  • Layout weightを空にします。
  • Layout widthに「wrap_content」を指定します。

 Layout weightを空にすることでこのボタンは表示に足りるだけの大きさに調整されます。

 Text属性はこのボタンに表示するラベルの文字列を指定します。@string/find_buttonというリソースIDはこの段階ではまだ定義していないため、このままではリソースIDがそのままボタンに表示されてしまいます。

 Package Explorerからstrings.xmlをダブルクリックして開き、文字列リソースを追加します。不要になった@string/hello文字列リソースは削除し、リスト2-4のように編集してください。

  • リスト2-4 strings.xml :
    < xml version="1.0" encoding="utf-8" >
    <resources>
    
        <string name="app_name">FFind</string>
        <string name="find_button">検索</string>
    
    </resources>

 最後に検索結果を表示するビューを追加します。本来表示用として適切ではありませんが、説明を簡単にするためテキストボックスをここでも使用します。再びPaletteからText Fieldsをクリックして展開し、プレーンテキストボックスをFindTarget/FindButtonの下部にドラッグ&ドロップで配置します。こちらのテキストボックスのプロパティは以下のようになります。

  • Idに「@+id/Result」と入力します。
  • Layout heightに「fill_parent」を指定します。
  • Layout widthに「fill_parent」を指定します。
  • Editableに「false」を設定します。

 Layout height/Layout widthにfill_parent(API level 8以降ではmatch_parent)を指定することで、画面の残りをこのテキストボックスが占有することになります。
 このテキストボックスは結果表示を行うだけのものなので、Editableにfalseを設定して編集不可能にしています。

 ここまででOutlineは以下のようになっているはずです。親子関係がおかしい場合、Outline上で各ビュー名をドラッグ&ドロップすることで調整が出来ます。

 ・LinearLayout
  ・linearLayout1
   ・FindTarget
   ・FindButton
  ・Result

 以上でレイアウト作成は完了です。アプリケーションを実行して、作成したレイアウトが反映されているかを確認して下さい。

2-4. 検索処理の実装

 ここからはAndroidのファイルシステムを走査し、指定のキーワードを持つファイルをリストアップする処理を実装します。

 Package ExplorerでFFindActivity.javaをダブルクリックして開き、onCreateメソッドの後に以下のprivateメソッドを追加します。

  • リスト2-5 :
    private void listFile(String target, String folder, ArrayList<String> list) {
         File f = new File(folder);
         File fl[] = f.listFiles();
         if (fl != null) {
             for (int i = 0; i < fl.length; ++i) {
                  if (list.size() == 5000) {
                       break;
                  }
    
                  File tmp = fl[i];
                  String name = tmp.getName();
                  if (name.contains(target) == true) {
                      list.add(name);
                  }
                  if (tmp.isDirectory() == true) {
                       try {
                            String c = tmp.getAbsolutePath();
                            if (c.equals(tmp.getCanonicalPath()) == true) {
                                if (c != null) {
                                    listFile(target, c, list);
                                }                               
                           }
                       }
                       catch (Exception e) {
                            e.printStackTrace();
                       }
                  }
             }
         }
    }

 このメソッドはfolderで指定されたフォルダのファイル一覧を取得し、targetで指定されたキーワードに一致する物があればlistに追加を行う再帰的メソッドになっています(検索結果は最大5000件までとしています)。FileクラスのlistFilesメソッドで一覧を取得し、リストの各ファイルについてgetNameで取得したファイル名をチェックし、targetが含まれていればリストに追加しています。
 isDirectoryメソッドでフォルダであるかどうかをチェックし、フォルダであれば同ファイルの絶対パスを対象としてlistFileメソッドを呼び出していますが、getCanonicalPathメソッドとの比較がfalseとなった場合はsymbolic linkと判断し、再帰呼び出しを行なっていません。

 ここで使用しているFileクラスとArrayListクラスを利用可能にするため、両パッケージをandroid.app.Activityの前でインポートします。

import java.io.File;
import java.util.ArrayList;

 次に、検索結果を格納するため、onCreateメソッドの直前で以下のメンバ変数を追加します。ArrayListは指定クラスのオブジェクトを要素に持つ配列リストのテンプレートクラスです。

private ArrayList<String> mList;

 onCreateのsetContentView呼び出し後にこの配列リストを生成し、検索ボタンの処理を実装します(リスト2-6)。

  • リスト2-6 :
    mList = new ArrayList<String>();
    
    Button button = (Button) findViewById(R.id.FindButton);
    if (button != null) {
         button.setOnClickListener(new View.OnClickListener() {
              @Override
              public void onClick(View view) {
                  if (view.getId() == R.id.FindButton) {
                       EditText text;
                       text = (EditText) findViewById(R.id.FindTarget);
                       String target = text.getText().toString();
                       if (target.equals("") != true) {
                           mList.clear();
                           listFile(target, "/", mList);
                           if (mList.size() > 0) {
                               String tmp = "";
                               for (int i = 0; i < mList.size(); ++i) {
                                   tmp += mList.get(i) + "\n";
                               }
                               text = (EditText) findViewById(R.id.Result);
                               text.setText(tmp);
                           }
                       }
                  }
              }
         });
    }

 findViewByIdメソッドはアクティビティやビューが持っている子ビューをリソースIDで検索して返すメソッドで、ここでは「R.id.FindButton」を指定して検索ボタンを取得しています。
 ボタンの取得に成功した場合、setOnClickListenerメソッドでクリック時に実行される処理をセットします。ここでは無名のインターフェースクラスを生成し、同メソッドの引数に渡しています。

 onClickが実際にボタンの処理を実装するメソッドになります。このメソッドにはクリックを引き起こしたビューが引数として渡されるため、getIdメソッドで取得したリソースIDが検索ボタンの物であるかをチェックし、検索処理を行なっています。
 検索処理ではFindTargetテキストボックスからgetTextメソッドで取得した内容をキーワードに、ルートフォルダを検索フォルダに指定してlistFileメソッドを呼び出しています。キーワードが空の場合は検索を実行せず、また検索前に結果リストをクリアしている点にも注意して下さい。

 最後に結果リストの内容をチェックし、空でなければリストの内容をResultテキストボックスにセットします。

 以上で実装は完了です。早速実行してみましょう。

2-5. ANR問題とアプリケーションのマルチスレッド化

 前項で完成したアプリケーションですが、検索結果が多くなると図2-4のようなエラーが発生して落とされてしまうことがあります。

02_04.png
図2-4


 これはANR(Application Not Responding)と呼ばれ、Androidのポリシーにより発生する現象です。

 Androidアプリケーションは通常、特に何も意識せずに作成した場合、UIの描画を含む全ての処理がメインスレッドと呼ばれる単一のスレッドで動作します。一方で、Androidはこのメインスレッドが5秒以上応答しない場合、先のANRを引き起こしアプリケーションを強制的に終了させようとします。

 Androidは元々スマートフォン用のOSとして開発されたため、システム全体のレスポンスを第一に考えて設計されています。このため、長時間応答を止めてしまうようなアプリケーションは許されません。仮にそのようなアプリケーションがインストールされた場合でも、システム側で強制的に止められるようにすることで、電話がかかってきたのに出られない、といった致命的な問題が起きないようになっているのです。

 またアプリケーション単体で考えても、メインスレッドに時間のかかる処理を行わせると「ボタンを押しても反応が無い」「長時間画面が固まる」といった印象をユーザーに与えかねないので避けるべきでしょう。

 こうした問題を避けるため、時間のかかる処理はメインスレッドとは分離し、バックグラウンドで実行する必要が出てきます。そこでここからは先ほど作成したアプリケーションを、クリック時に行う処理を別スレッドで実行するよう修正していきます。リスト2-7がonClick内の処理を別スレッド化した物になります。

  • リスト2-7 :
    public void onClick(View view) {
        if (view.getId() == R.id.FindButton) {
             Thread t = new Thread() {
                  @Override
                  public void run() {
                     EditText text;
                     text = (EditText) findViewById(R.id.FindTarget);
                     String target = text.getText().toString();
                     if (target.equals("") != true) {
                          mList.clear();
                         listFile(target, "/", mList);
                         if (mList.size() > 0) {
                             String tmp = "";
                             for (int i = 0; i < mList.size(); ++i) {
                                  tmp += mList.get(i) + "\n";
                             }
                             text = (EditText) findViewById(R.id.Result);
                             text.setText(tmp);
                         }
                     }
                  }
             };
             t.start();
        }
    }

 onClickでFindButtonかどうか確認した後、Threadクラスのオブジェクトを生成し、オーバーライドしたrunメソッドの中に検索処理を移しています。スレッドのstartメソッドにより別スレッドが開始されて、runメソッドがそこで実行されます。
 時間のかかる処理が別スレッドに移動したため、検索ボタンを押すとすぐ画面操作が行えるようになります。しかし、AndroidではUIに関わる描画処理はメインスレッドでしか行えないようになっているため、Resultテキストボックスに結果をセットする際にアプリケーションエラーが起きてしまいます(図2-5)。

02_05.png
図2-5


 時間のかかる処理は別スレッドで行わせつつ、処理が完了した後の描画処理をメインスレッドで実行する必要があります。これにはいくつかの解決法がありますが、ここではHandlerクラスのpostメソッドを使用します。
 Handlerクラスを利用可能にするため、android.os.Bundleのインポート行の後に以下を追加します。

import android.os.Handler;

 FFindActivityのprivateメンバに結果表示用のStringとHandlerクラスを追加します。

private String mResult;
private Handler mHandler = new Handler();

 onClickのうちUIを操作する必要がある箇所を修正します(リスト2-8)。

  • リスト2-8 :
    public void onClick(View view) {
        if (view.getId() == R.id.FindButton) {
             Thread t = new Thread() {
                  @Override
                  public void run() {
                     EditText text;
                     text = (EditText) findViewById(R.id.FindTarget);
                     String target = text.getText().toString();
                     if (target.equals("") != true) {
                         mList.clear();
                         listFile(target, "/", mList);
                         if (mList.size() > 0) {
                             mResult = "";
                             for (int i = 0; i < mList.size(); ++i) {
                                  mResult += mList.get(i) + "\n";
                             }
                             mHandler.post(new Runnable() {
                                  @Override
                                  public void run() {
                                    EditText text = (EditText) findViewById(R.id.Result);
                                    text.setText(mResult);
                                  }
                             });
                         }
                     }
                  }
             };
             t.start();
        }
    }

 検索結果のサイズを確認した後、リスト内のデータを結果表示用文字列に格納し、HandlerのpostメソッドでRunnableなインターフェースクラスをメインスレッドに送信しています。Runnableを受信したメインスレッドがこのrunメソッドを呼び出し、同メソッド内に実装された処理を実行します。

 これで時間のかかる検索処理を別スレッドに追い出し、検索終了後にメインスレッドで結果表示を行うことが出来ました。

2-6. AsyncTaskによるマルチスレッド処理

 前項ではThreadとHandlerを使用して時間のかかる処理をメインスレッドから分離しましたが、Androidではこうした処理の切り分けを簡単に実装することの出来るAsyncTaskというユーティリティクラスが提供されています。FFindをこのAsyncTaskを利用した物に作り替えてみましょう。

 AsyncTaskにはバックグラウンドで行う必要がある処理を実装するメソッドやバックグラウンド処理の完了後に呼び出されるメソッドが用意されており、これらをオーバーライドすることで非同期処理を簡単に実現出来るようになっています。代表的な物を以下に挙げていきます。

  • onPreExecute
    • バックグラウンド処理の実行前に呼び出される前処理用のメソッドで、メインスレッドで実行されます。
  • doInBackground
    • バックグラウンド処理用のメソッドで、メインスレッドとは別スレッドで実行されます。
  • onProgressUpdate
    • バックグラウンド処理の進行状況更新時に呼び出されるメソッドで、メインスレッドで実行されます。
  • onPostExecute
    • バックグラウンド処理の完了後に呼び出される後処理用のメソッドで、メインスレッドで実行されます。

 ここからは実際にAsyncTaskのサブクラスを作成し、FFindに必要な処理を実装していきます。EclipseのPackage ExplorerでFFindをクリックして選択し、メニューから「File > New > Class」を選択します。

02_06.png
図2-6


 図2-6のダイアログが表示されたら、Packageに「com.beatcraft.ffind」、Nameに「FindTask」、Superclassに「android.os.AsyncTask<String, Integer, Void>」と入力して「Finish」ボタンをクリックします。これでFindTask.javaというJavaソースコードが自動生成され、FFindのプロジェクトに追加されます。

 Superclassの欄に入力した通り、AsyncTaskは3つの型を指定するテンプレートクラスになっており、1つめはdoInBackgroundメソッドの引数の型、2つめはonProgressUpdateメソッドの引数の型、3つめはonPostExecuteメソッドの引数の型を指定します。

 作成されたFindTask.javaをダブルクリックして開き、メンバ変数とコンストラクタを追加します(リスト2-9)。

  • リスト2-9 :
    private FFindActivity mActivity;
    
    public FindTask(FFindActivity activity) {
        mActivity = activity;
    }

 検索処理と結果の反映をFFindActivityで行うため、これを保持するための変数を追加しています。コンストラクタでFFindActivityを引数に取り、追加した変数に格納します。
 続いてdoInBackgroundの処理を実装します(リスト2-10)。

  • リスト2-10 :
    @Override
    protected Void doInBackground(String... arg0) {
        String target = arg0[0];
        mActivity.listFile(target);
        return null;
    }

 doInBackgroundの引数はテンプレートで指定した型の可変長引数になっているので、ここから一つ目の要素を取り出し、検索キーワードとしてFFindActivityのlistFileメソッドに渡しています。ここで呼び出しているlistFileメソッドはこれまでに実装した検索メソッドではなくFindTaskから検索処理を呼び出すために利用するもので、この後で実装していきます(引数が異なることに注意してください)。
 引き続きonPostExecuteメソッドを実装します(リスト2-11)。

  • リスト2-11 :
    @Override
    protected void onPostExecute(Void v) {
        mActivity.result();
        mActivity = null;
    }

 onPostExecuteは検索が完了した際に呼び出されるので、FFindActivityのresultメソッドを呼び出して検索結果の反映を行なっています(resultメソッドは後ほど実装します)。

 以上でFindTaskの実装はひとまず完了です。続いてFFindActivityを、このFindTaskを利用した形に修正していきます。FindButtonにOnClickListenerを設定する際のonClickを、以下のように修正します(リスト2-12)。

  • リスト2-12 :
    public void onClick(View view) {
         if (view.getId() == R.id.FindButton) {
             EditText text;
             text = (EditText) findViewById(R.id.FindTarget);
             String target = text.getText().toString();
    
             if (target.equals("") == false) {
                  mList.clear();
                  FindTask task = new FindTask(FFindActivity.this);
                  task.execute(target);
             }
         }
    }

 キーワードが空でなかった場合FindTaskを生成し、executeメソッドを呼び出してバックグラウンド処理を開始しています。この時、executeメソッドに渡した引数がdoInBackgroundに引き渡されます。ThreadとHandlerで行なっていた処理をAsyncTaskに統合したため、非常にすっきりとした記述になりました。メンバ変数のmHandlerは不要となったので、こちらの宣言とパッケージインポートも削除しておきます。

 FindTaskから呼び出される検索処理と結果表示のメソッドを追加します。

  • リスト2-13 :
    public void listFile(String target) {
        listFile(target, "/", mList);
        if (mList.size() > 0) {
            mResult = "";
            for (int i = 0; i < mList.size(); ++i) {
                mResult += mList.get(i) + "\n";
            }
        }
    }
    
    public void result() {
        if (mList.size() > 0) {
            EditText text = (EditText) findViewById(R.id.Result);
            text.setText(mResult);
        }
        else {
            Toast.makeText(this, "* NOT FOUND *", Toast.LENGTH_SHORT).show();
        }
    }

 FindTaskのdoInBackgroundから呼び出されたlistFileメソッドでは、渡されたキーワードを検索対象、ルートフォルダ("/")を検索開始フォルダとして実際の検索処理を呼び出したあと、結果格納用のStringに整形して格納しています。doInBackgroundはメインスレッドとは別のスレッドから呼び出されるため、この処理が長時間かかってもアプリケーションがANRで停止されることはありません。

 FindTaskのonPostExecuteから呼び出されたresultメソッドでは検索結果の文字列をResultテキストボックスに設定しています。onPostExecuteはメインスレッドで呼び出されるため、EditTextのsetTextを実行してもエラーが起きることはありません。

 また、検索結果が0件だった場合何も表示しないのはわかりづらいため、Toastを使用して通知するようにしました(図2-7)。Toastはユーザーからのレスポンスを必要としない簡単なメッセージを表示するポップアップダイアログで、特定の処理を通過した際に表示するなど簡易デバッグツールとしても利用出来ます(但し、Toastもメインスレッドからでなければ利用出来ないため注意が必要です)。
 尚、Toastの使用にはandroid.widget.Toastパッケージのインポートが必要です。

02_07.png
図2-7


 FFindのAsyncTask化はこれで完了ですが、よりわかりやすく、検索中はプログレスダイアログを表示するようにしてみましょう。

 FFindTask.javaを開き、プログレスダイアログのパッケージをインポートします。

import android.app.ProgressDialog;

 プログレスダイアログをメンバ変数に追加します。

private ProgressDialog mDialog;

 onPreExecuteメソッドを追加し、プログレスダイアログの生成と表示を行います(リスト2-14)。

  • リスト2-14 :
    @Override
    protected void onPreExecute() {
        mDialog = new ProgressDialog(mActivity);
        mDialog.setMessage(mActivity.getString(R.string.find_progress));
        mDialog.show();
    }

 ダイアログのsetMessageメソッドはダイアログに表示するメッセージを設定するメソッドです。ここで指定しているR.string.find_progressは後ほど文字列リソースに追加します。

 onPostExecuteメソッドを修正し、プログレスダイアログを消去します(リスト2-15)。

  • リスト2-15 :
    @Override
    protected void onPostExecute(Void v) {
        mDialog.dismiss();
        mDialog = null;
        mActivity.result();
        mActivity = null;
    }

 リソースのstrings.xmlをダブルクリックして開き、以下の文字列リソースを追加します。

<string name="find_progress">検索中...</string>

 以上で修正は完了です。検索中は図2-8のようなダイアログが表示され、検索が終了すると自動的に消えることを確認して下さい。

02_08.png
図2-8


 プログレスダイアログは現在の進捗状況をバーで示すなど、いくつかのスタイルが使用可能です。AsyncTaskのonProgressUpdateメソッド内でダイアログの進捗状況を更新し、doInBackgroundでの処理進行状況に応じてpublishProgressメソッドを適宜呼び出すことで、バックグラウンド処理の進捗に応じた表示が行えます。

2-7. アクティビティの遷移

 ここからはFFindの改修を更に進めて、検索結果を別アクティビティに表示し、結果表示もリストビューを使用した物に整えていきます。

 まずは結果表示用のアクティビティを用意します。EclipseのPackage ExplorerでFFindをクリックして選択し、メニューから「File > New > Class」を選択します。クラス作成ダイアログが表示されたら、Packageに「com.beatcraft.ffind」、Nameに「ResultActivity」、Superclassに「android.app.Activity」と入力して「Finish」ボタンをクリックします。

 作成されたResultActivity.javaをダブルクリックして開き、onCreateメソッドを実装していきます(リスト2-16)。Bundleクラスを使用可能にするため、android.os.Bundleパッケージのインポートも行なって下さい。

  • リスト2-16 :
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.result);
    }

 続いてこのアクティビティで使用するレイアウト(R.layout.result)を作成します。FFindをクリックして選択し、メニューから「File > New > Other...」を選択します。"Select a wizard"ダイアログ(図2-9)が表示されたら、「Android > Android XML Layout File」を選択して「Next」ボタンをクリックして下さい。

02_09.png
図2-9


 続く"New Android Layout XML File"でFileに「result.xml」と入力し、「Finish」ボタンをクリックするとレイアウトリソースファイルが作成されます。
 Package Explorerからresult.xmlをダブルクリックして開き、作成されたレイアウトにリストビューを追加します。PaletteからCompositeをクリックして展開し、ListViewをドラッグ&ドロップで配置します。ListViewは黒塗りで特に何も表示されないためわかりづらいのですが、Composite内の一番最初の項目がListViewになっています(図2-10)。

02_10.png
図2-10


 配置したリストビューのプロパティを以下のように設定します。

  • Idに「@+id/Result」と入力します。
  • Layout heightに「fill_parent」を指定します。
  • Layout widthに「fill_parent」を指定します。

 以上でResultアクティビティのレイアウト作成は完了です。Outlineが以下のようになっていることを確認して下さい。

 ・LinearLayout
  ・Result

 Resultアクティビティの実装を進める前に、FFindActivityの修正を行います。FFindActivityでは結果表示を行わなくなるため、まずはレイアウトmainからResultテキストボックスを削除します。

 次にFFindActivity.javaを開き、検索ボタンをクリックされた際の処理を変更します(リスト2-17)。

  • リスト2-17 :
    @Override
    public void onClick(View view) {
        if (view.getId() == R.id.FindButton) {
            EditText text;
            text = (EditText) findViewById(R.id.FindTarget);
            String target = text.getText().toString();
    
            if (target.equals("") == false) {
                Intent intent = new Intent(getApplicationContext(),
                    com.beatcraft.ffind.ResultActivity.class);
                intent.putExtra("Target", target);
                startActivity(intent);
            }
        }
    }

 FindTaskを生成し、検索処理を実行していた箇所が変更されています。Intentはアクティビティやアプリケーション間で情報をやり取りするためのメッセージオブジェクト「インテント」を取り扱うクラスで、ここではFFindActivityからResultActivityを呼び出すために使用しています。二番目の引数に「com.beatcraft.ffind.ResultActivity.class」と渡すことで、このインテントの送信先を明示的に指定しています。
 続くputExtraメソッドでは、インテントを送信する際に添付する情報を追加しています。ここでは検索キーワードである文字列targetを、「Target」という名前をつけて添付しています。
 最後にstartActivityでインテントを送信し、ResultActivityを呼び出します。

 これまでFFindActivityで行なっていた検索処理と結果表示はResultActivityで行うため、以下のメンバ変数と3つのメソッドをFFindActivityからは削除し、ResultActivityに移動します。FFindActivityのonCreateで行なっていたmListの生成処理とStringメンバ変数mResultも不要となるためあわせて削除して下さい。

private ArrayList<String> mList;
public void listFile(String target)
public void result()
private void listFile(String target, String folder, ArrayList<String> list)

 これでFFindActivity.javaの修正は完了です。続いてResultActivity.javaを開き、必要な処理の追加とFFindActivityから移植したメソッドの修正を行います。

 まずはonCreateを修正します。setContentView呼び出しのあとに、次の内容を追加します(リスト2-18)。

  • リスト2-18 :
    mList = new ArrayList<String>();
             
    Intent intent = getIntent();
    if (intent == null) {
        finish();
    }
    String target = intent.getStringExtra("Target");
    FindTask task = new FindTask(this);
    task.execute(target);

 最初にFFindActivityから引き継いだArrayListの生成を行なったあと、続くgetIntentメソッドでアクティビティ呼び出し時に送信されたインテントを取得しています。このインテントから検索キーワードを受け取るため、みつからなかった場合にはfinishメソッドでこのアクティビティを終了させます。
 getStringExtraはインテントから文字列タイプの添付データを取得するメソッドで、ここでは「Target」という名前の添付データを取得し、これをキーワードとしてFindTaskのファイル検索を実行しています。

 FindTaskから呼び出されるlistFileはprivateのlistFileメソッドのみに変更します(リスト2-19)。

  • リスト2-19 :
    public void listFile(String target) {
        listFile(target, "/", mList);
    }

 resultメソッドでは結果の設定をEditTextからListViewの物へ変更を行います(リスト2-20)。

  • リスト2-20 :
    public void result() {
        if (mList.size() > 0) {
            ListView lv = (ListView) findViewById(R.id.Result);
            String sa[] = new String[mList.size()];
            for (int i = 0; i < mList.size(); ++i) {
                sa[i] = mList.get(i);
            }
            ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
                android.R.layout.simple_list_item_1, sa);
            lv.setAdapter(adapter);
        }
        else {
            Toast.makeText(this, "* NOT FOUND *", Toast.LENGTH_SHORT).show();
        }
    }

 ListViewはアダプターと呼ばれるクラスでリストデータと結び付けられます。アダプターにはいろいろな種類がありますが、ここでは文字列配列を取り扱うため、配列用アダプターのテンプレートクラスArrayAdapterを使用しています。
 ArrayAdapterではリストビューの各要素(リストアイテム)を表示する際に使用するレイアウトを指定することが出来ますが、今回使用している「android.R.layout.simple_list_item_1」はAndroidが標準で用意しているレイアウトリソースで、TextViewを1つ含むだけの単純なレイアウトになっています。このTextViewにリストデータ中の各文字列が表示されることになります。
 ArrayListから文字列配列を作成し、このデータをもとに作成したArrayAdapterをListViewのsetAdapterメソッドで関連付けています。

 最後にFindTask.javaを開き、FFindActivityとなっている箇所を全てResultActivityに書き換えればソースコードの修正は完了です。

 残る作業はマニフェストの修正です。追加したResultActivityを利用可能にするには、マニフェストでこれを宣言しなければいけません。Package ExplorerからAndroidManifest.xmlを開き、FFindActivityを宣言しているactivity要素のあとに以下を追加します(リスト2-21)。

  • リスト2-21 :
    <activity
        android:label="@string/app_name"
        android:name=".ResultActivity" >
    </activity>

 以上で全ての修正が完了しました。早速実行してみましょう(図2-11)。

02_11.png
図2-11


2-8. FFind全ソースコード/XMLファイル

  • リスト2-22 FFindActivity.java :
    import android.os.Bundle;
    import android.view.View;
    import android.widget.Button;
    import android.widget.EditText;
    
    public class FFindActivity extends Activity {
        /** Called when the activity is first created. */
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.main);
    
            Button button = (Button) findViewById(R.id.FindButton);
            if (button != null) {
                 button.setOnClickListener(new View.OnClickListener() {
                      @Override
                      public void onClick(View view) {
                           if (view.getId() == R.id.FindButton) {
                               EditText text;
                               text = (EditText) findViewById(R.id.FindTarget);
                               String target = text.getText().toString();
    
                               if (target.equals("") == false) {
                                    Intent intent = new Intent(getApplicationContext(),
                                       com.beatcraft.ffind.ResultActivity.class);
                                    intent.putExtra("Target", target);
                                    startActivity(intent);
                               }
                           }
                      }
                 });
            }
        }
    }


  • リスト2-23 ResultActivity.java :
    package com.beatcraft.ffind;
    
    import java.io.File;
    import java.util.ArrayList;
    
    import android.app.Activity;
    import android.content.Intent;
    import android.os.Bundle;
    import android.widget.ArrayAdapter;
    import android.widget.ListView;
    import android.widget.Toast;
    
    public class ResultActivity extends Activity {
         private ArrayList<String> mList;
    
         @Override
         public void onCreate(Bundle savedInstanceState) {
              super.onCreate(savedInstanceState);
              setContentView(R.layout.result);
    
            mList = new ArrayList<String>();
             
              Intent intent = getIntent();
              if (intent == null) {
                   finish();
              }
              String target = intent.getStringExtra("Target");
              FindTask task = new FindTask(this);
              task.execute(target);
         }
    
        public void listFile(String target) {
             listFile(target, "/", mList);
        }
    
        public void result() {
              if (mList.size() > 0) {
                   ListView lv = (ListView) findViewById(R.id.Result);
                   String sa[] = new String[mList.size()];
                   for (int i = 0; i < mList.size(); ++i) {
                        sa[i] = mList.get(i);
                   }
                   ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
                        android.R.layout.simple_list_item_1, sa);
                   lv.setAdapter(adapter);
              }
              else {
                   Toast.makeText(this, "* NOT FOUND *", Toast.LENGTH_SHORT).show();
              }
        }
    
        private void listFile(String target, String folder, ArrayList<String> list) {
             File f = new File(folder);
             File fl[] = f.listFiles();
             if (fl != null) {
                 for (int i = 0; i < fl.length; ++i) {
                      if (list.size() == 5000) {
                           break;
                      }
    
                      File tmp = fl[i];
                      String name = tmp.getName();
                      if (name.contains(target) == true) {
                          list.add(name);
                      }
                      if (tmp.isDirectory() == true) {
                           try {
                                String c = tmp.getAbsolutePath();
                                if (c.equals(tmp.getCanonicalPath()) == true) {
                                    if (c != null) {
                                        listFile(target, c, list);
                                    }                               
                               }
                           }
                           catch (Exception e) {
                                e.printStackTrace();
                           }
                      }
                 }
             }
        }
    }


  • リスト2-24 FindTask.java :
    package com.beatcraft.ffind;
    
    import android.app.ProgressDialog;
    import android.os.AsyncTask;
    
    public class FindTask extends AsyncTask<String, Integer, Void> {
         private ResultActivity mActivity;
         private ProgressDialog mDialog;
    
         public FindTask(ResultActivity activity) {
              mActivity = activity;
         }
    
         @Override
         protected void onPreExecute() {
              mDialog = new ProgressDialog(mActivity);
              mDialog.setMessage(mActivity.getString(R.string.find_progress));
              mDialog.show();
         }
       
         @Override
         protected Void doInBackground(String... arg0) {
              String target = arg0[0];
              mActivity.listFile(target);
              return null;
         }
    
         @Override
         protected void onPostExecute(Void v) {
              mDialog.dismiss();
              mDialog = null;
              mActivity.result();
              mActivity = null;
         }
    }


  • リスト2-25 main.xml :
    <?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:id="@+id/linearLayout1"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" >
    
            <EditText
                android:id="@+id/FindTarget"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:singleLine="true" >
    
                <requestFocus />
            </EditText>
    
            <Button
                android:id="@+id/FindButton"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@string/find_button" />
        </LinearLayout>
    
    </LinearLayout>


  • リスト2-26 result.xml :
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical" >
    
        <ListView
            android:id="@+id/Result"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent" >
        </ListView>
    
    </LinearLayout>


  • リスト2-27 AndroidManifest.xml :
    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.beatcraft.ffind"
        android:versionCode="1"
        android:versionName="1.0" >
    
        <uses-sdk android:minSdkVersion="8" />
    
        <application
            android:icon="@drawable/ic_launcher"
            android:label="@string/app_name" >
            <activity
                android:label="@string/app_name"
                android:name=".FFindActivity" >
                <intent-filter >
                    <action android:name="android.intent.action.MAIN" />
    
                    <category android:name="android.intent.category.LAUNCHER" />
                </intent-filter>
            </activity>
            <activity
                android:label="@string/app_name"
                android:name=".ResultActivity" >
            </activity>
        </application>
    
    </manifest>

内藤

添付ファイル: file02_11.png 754件 [詳細] file02_10.png 754件 [詳細] file02_09.png 761件 [詳細] file02_08.png 707件 [詳細] file02_07.png 678件 [詳細] file02_06.png 759件 [詳細] file02_05.png 755件 [詳細] file02_04.png 679件 [詳細] file02_03.png 659件 [詳細] file02_02.png 666件 [詳細] file02_01.png 793件 [詳細]

BC::labsへの質問は、bc9-dev @ googlegroups.com までお願い致します。
トップ   編集 凍結 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   最終更新のRSS
Last-modified: 2011-12-12 (月) 20:44:19 (2998d)