Android Binding 〜SDカードファイルビュー〜

最近風邪でダウンしていましたが、調子が戻って来たので棚上げにしていた問題にチャレンジしてみました。問題とは、Android Bindingを使って自分のアプリを書き直す、というもの。

いきなりフルスペックで書き直すのは厳しいので、ちょっとずつ書き直していきます。まずは土台となる部分です。

  1. ファイル (フォルダ) 名をリスト表示
  2. フォルダのリストをタップするとフォルダの中身をリストアップ
  3. BACKキーで階層を上がる


とりあえずこの辺りを目標とします。ここまで出来れば後は一つ一つ足りない機能を追加していけば何とかなりそうです。

まずはSDカードのトップディレクトリ下をリストアップします。

と言いつつ、始めはお決まりのコードです。

Applicationクラス

public class TestExplorerApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();

        // Android Bindingを初期化する
        Binder.init(this);
    }
}


Activityクラス

public class TestExplorer extends Activity {

    private TestExplorerViewModel _model = null;
    private String _currentPath = Environment.getExternalStorageDirectory().getPath();

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // ViewとView Modelをバインド
        _model = new TestExplorerViewModel();
        Binder.setAndBindContentView(this, R.layout.main, _model);
    }
}


ViewModelクラス

public class TestExplorerViewModel {
}


レイアウトファイル

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:binding="http://www.gueei.com/android-binding/"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent" >

    <ListView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content" />
		
</LinearLayout>


ここから始めましょう。ここからどんどんバインドして行きましょう。

まずはListViewをバインドします。

レイアウトファイル

    <ListView
        binding:itemSource="DirectoryEntryList"
        binding:itemTemplate="@layout/dir_item"
        binding:onItemClicked="OnItemClicked"
        binding:clickedItem="ClickedItem"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content" />


どれも出てきたものばかりですね。ちょっとまとめてみましょうかね。

属性 意味
itemSource リスト表示の元ネタプロパティ
itemTemplate リストの各項目のレイアウト定義
onItemClicked リストクリック時のコマンド
clickedItem クリックされたリスト項目


上から一つ一つ見ていきましょう。まずはitemSourceです。

ViewModelクラス

public class TestExplorerViewModel {
    public ArrayListObservable<DirectoryEntry> DirectoryEntryList =
         new ArrayListObservable<DirectoryEntry>(DirectoryEntry.class);

    public class DirectoryEntry {
        public StringObservable Name = new StringObservable();
    }
}


以前は次のように使っていました。

public ArrayListObservable<String> AsiaList = new ArrayListObservable<String>(String.class);


今回は表示する内容が単純な文字列ではないため、独自のクラス (DirectoryEntry) を定義して、ArrayListObservableのパラメータに指定しています。と言いつつ、今のところは文字列 (StringObservable) だけですけどね。

リストの場合、これだけでは足りません。外からリストを初期化してあげないとイケません。こんな感じのメソッドを追加してみました。

ViewModelクラス

private String _currentPath = null;

public void setPath(String path) {

    File file = new File(path);
    if(file.isFile()) {
        finish();
        return;
    }

    File[] files = file.listFiles();
    if(files.length <= 0)
        return;

    _currentPath = path;

    DirectoryEntry[] entries = new DirectoryEntry[files.length];
    for(int i = 0; i < files.length; i ++) {
        DirectoryEntry entry = new DirectoryEntry();

        String name = files[i].getName();
        entry.Name.set(name);

        entries[i] = entry;
    }

    DirectoryEntryList.setArray(entries);
}


フルパスを指定するとそのパス下に存在するエントリを配列化して、ArrayListObservableオブジェクトに設定します。

次はitemTemplateです。内容はこんな感じ。

リスト内レイアウト定義 (dir_item.xml)

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:binding="http://www.gueei.com/android-binding/"
    android:orientation="horizontal"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">

    <TextView
        android:textSize="16sp"
        android:textColor="#FEFEFE"
        android:singleLine="true"
        android:ellipsize="marquee"
        binding:text="Name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</LinearLayout>


細かいところはさておき、ここで重要なのは以下の属性です。

    <TextView
        ...
        binding:text="Name"
        ...
        />


このNameは実は先ほど記載したDirectoryEntryクラスのpublicフィールドであるNameとバインドするためのものです。

誤解を恐れず書くと、こんな感じの対応関係が見て取れます。

View ViewModel
ListView DirectoryEntryList
dir_item.xml DirectoryEntry
TextView Name


DirectoryEntryにバインドするためのpublicフィールドをどんどん追加していけば、豪華なリスト表示になるわけです。

次はonItemClickedです。リストがクリックされた時のアクション (Command) です。こんな感じにしてみました。

ViewModelクラス

public Command OnItemClicked = new Command() {
    @Override
    public void Invoke(View view, Object... args) {
        if(_currentPath == null)
            return;

        DirectoryEntry clicked = (DirectoryEntry)ClickedItem.get();
        if(clicked == null)
            return;

        if(clicked.IsFile.get() == true)
            return;

        String path = _currentPath + "/" + clicked.Name.get();
        setPath(path);
    }
};


次に説明しますが、ClickedItemにはクリックしたリストの内容 (この場合、DirectoryEntryクラスのインスタンス) が自動的にセットされています。なので、それを取得して、ディレクトリであればパス文字列をつないで、先ほどのsetPathメソッドに渡しています。

う〜ん、_currentPathがちょっと美しくありませんが、一応問題なく動きます。

最後にclickedItem。これについては特に処理を記述することはありません。定義しておくだけでOK、あとはAndroid Binding側でよしなにしてくれます。

ViewModelクラス

public Observable<Object> ClickedItem = new Observable<Object>(Object.class);


残る要求仕様はBACKキーによる戻る遷移ですが、これが今のところ分からない。Android Bindingのソースコードを眺めていると、OnKeyListenerMulticastとかいうクラスがあるのでこれを使うのかなぁ、と思いつつ、使い方分からず。

とりあえず普通にActivity側で実装しておきます。もしAndroid Bindingシステムの中でハンドリング出来るようであれば、修正することにします。

Activityクラス

@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
    if(keyCode == KeyEvent.KEYCODE_BACK) {
        _model.backPath();
        return true;
    }
    return super.onKeyDown(keyCode, event);
}


ViewModelクラス

public void backPath() {

    // SDカードのTOP階層から上にはイケないことにする
    if(_currentPath.equals(Environment.getExternalStorageDirectory().getPath()))
        return;

    // あり得ないはずだが、パスが「/」の場合は、上にはイケないことにする
    String[] tmp = _currentPath.split("\\/");
    if(tmp.length <= 1)
        return;

    // 文字列を分離して、末尾のみ切り離す
    String dest = "";
    for(int i = 1; i < tmp.length - 1; i ++) {
        dest += "/" + tmp[i];
    }

    // 一つ上の階層に移動する
    setPath(dest);
}


見た目はこんな感じ。いまいち花がないけど、まぁまずはこんなもんかな。


いつものようにソースコード一式はこちら。

サンプルコード置き場