Android Binding 〜SDカードファイルビュー〜
最近風邪でダウンしていましたが、調子が戻って来たので棚上げにしていた問題にチャレンジしてみました。問題とは、Android Bindingを使って自分のアプリを書き直す、というもの。
いきなりフルスペックで書き直すのは厳しいので、ちょっとずつ書き直していきます。まずは土台となる部分です。
- ファイル (フォルダ) 名をリスト表示
- フォルダのリストをタップするとフォルダの中身をリストアップ
- 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); }
見た目はこんな感じ。いまいち花がないけど、まぁまずはこんなもんかな。
いつものようにソースコード一式はこちら。