umjammer です、 弊社の Android 案件の成果物として、リッチなUIを簡単に使用することができるUIコンポーネント群、名付けて「KLab iPhoroid UI」をここに発表します。
KLab iPhoroid UI Logo


Android 案件を進めていく上で、お客様から言われる要件の一つとして「iPhone と同じようなUIにしてもらえませんでしょうか?」というのがあります。やはりスマートフォンと言えば iPhone というイメージが世間では強いのでしょうか? Android には Android UI のポリシーが云々というのはありますが、そこはお客様の要望なので... そして Android のデフォルトで用意されている UI のみでは iPhone の様なリッチな UI & UX を再現するのは困難です。 サードパーティーも頑張って様々なリッチな UI を WEB に発表していますが、イメージの多用、メモリ管理や、大量データの適用等さすがに商用プロダクトとしてそのまま使用できるものはほとんど見当たりません。そんな中頑張って要求された仕様を満たしていった結果、できるだけ今までの Android のプログラミング流儀にのっとったままリッチな UI & UX を実現するコンポーネント群を開発することができました。 そして本日、そのまま商用プロダクトとして使用に耐えうる UI コンポーネント群をオープンソースソフトウェアとして公開します! 早速、作成したコンポーネントをリストアップしていきます。 ListView おなじみリストビューです。無限にページングできます。twitter リストのように上に引っ張れば新規アイテムを取得する処理を実装することも可能です。
iPhoroid ListView


特徴は、
  • twitter ライクな上オーバースクロールによる新規行取得
  • 下オーバースクロールによる無限ページング機能
  • イメージのキャッシュ及びメモリ管理機能
  • スクロール時に無駄な読み込みを抑制する機能
  • 画像読み込み時に無駄な描画を抑える機能
FlowView イメージビューアとして使用できます。ListView や GridView の詳細画面としても使用できます。フリックでアイテムを前後できますのでわざわざ ListView に戻らなくてもアイテムの移動が可能です。
iPhoroid FlowView


特徴は、
  • イメージのキャッシュ及びメモリ管理機能
  • ListViewと同等なOnScrollListenerのイベント機構組み込み
  • スクロール時に無駄な読み込みを抑制する機能
CoverFlow これもおなじみ、カバーフローです。 iPhone で作ったカバーフローよりスクロールスピードが早かったりします。
iPhoroid CoverFlow


特徴は、
  • ListViewと同等なOnScrollListenerのイベント機構組み込み
  • スクロール時に無駄な読み込みを抑制する機能
GridView これは Android オリジナルの UI そのままですが、 KLab iPhoroid UI が提供する Adapter を使用し、(後述)を実装することによって、今まで紹介した UI と同様、
  • ListViewと同等なOnScrollListenerのイベント機構組み込み
  • スクロール時に無駄な読み込みを抑制する機能
を実装することができます。
iPhoroid GridView


もう少し詳しく

キャッシュ&メモリ管理

Android 端末の少ないメモリでは ListView 等で無限にページングしていくと、アイテムの中に画像がある場合などすぐにメモリ不足に陥ってアプリケーションが OutOfMemoryError で落ちてしまいます。そこでデータは都度ネットワーク等からダウンロードして取得するという方向になるのですが、その都度ネットワークからダウンロードすると描画に時間がかかり UX としてよくありません。ここを解決するためにキャッシュを使用します。キャッシュも管理しないとどんどんメモリを食いつぶして同じ結果になるので、うまく不要なキャッシュを破棄する機構が必要になります。WEB を見るとキャッシュに SoftReference を利用しているものを見かけます。SoftReference は試したところ、確かにうまく機能するのですが期待していたよりかなり生存時間が短くなります。あまりキャッシュとしてはよろしくないので KLab iPhoroid UI は LRU アルゴリズムを採用したキャッシュを使用しています。生存時間を指定してキャッシュ機能を調整することもできます。 もう一つキャッシュの破棄に関して厄介なことがあります。Activity が画面遷移で変わってしまった場合裏に行った Activity 上のイメージ等のキャッシュは破棄するべきなのですが、バックボタンを押された時など突然表に戻ってきてしまう場合があります。その時キャッシュクリアにより Bitmap を recycle してしまっていると、使用中の画像がすでに recycle されてしまったと Error が起こりアプリケーションが落ちる場合があります。なので使用中かそうでないかを確認してキャッシュを破棄する機構を実装する必要があります。

無駄な処理の削減と見た目の改善

ListView は用意されている Adapter をそのまま使用すると、表示される以外のアイテムにも描画処理が走り、ネットワークからデータをロードする場合など無駄なロード処理が何度も走ります。これを抑制するためにデータをロードする AsyncTask を管理し、不必要なロードが走っているとキャンセルするようにします。またアイテムにイメージが含まれている場合何度も描画されちらつきます。これも ImageView にフラグを立てることによって無駄な描画処理が走らないように抑制します。 また KLab iPhoroid UI の ListView, FlowView, Coverflow, GridView 共通の機能としてフリック操作での高速スクロールがあります。フリックでスクロールを行なっているときにデータのローディングが発生すると結局スクロールアウトして無駄な処理になってしまいます。そこでフリックのスクロール中はデータをロードしない機構を組み込みます。FlowView, CoverFlow には ListView の様に OnScrollListener をオリジナルのままでは組み込むことができないのでフリック操作を検出するロジックを組み込み ListView と同等な OnScrollListener を使用することができるように改造してあります。 使用方法 長々と詳細を書きましたが使用方法は簡単です。

ListView

ListView には RefreshableArrayAdapter を extends したアダプタ(ここでは ListViewItemAdapter)を設定します。RefreshableArrayAdapter にアイテムを渡す処理で一つ注意点、オーバースクロールで新規アイテムが来たかどうかを判別するためにモデルクラス(ここでは Item)には equals() メソッドを正しく実装する必要があります。あとはデータを取得してくる処理でページングする個数ののアイテムを offset から返す処理(ここではListViewItemDao.getInstance().getItems())を書くだけで自動的に下オーバースクロールでページング、上オーバースクロールで新規アイテム取得処理を行ってくれます。

        :
        listView.setAdapter(new ListViewItemAdapter(ListViewActivity.this, items) {
            public List getItemsOnRefresh(int offset) throws IOException {
                return ListViewItemDao.getInstance().getItems(
                    offset == DefaultRefreshListener.PULL_TO_REFRESH ? 0 : offset);
            }
        });

ListView でイメージを扱う場合は通常通り Adapter.getView() 内でイメージを設定しますが、キャッシュの管理、データロードタスクの管理、フリックでのスクロール中にデータロードをしない機能を組み込むためにユーティリティクラスのメソッド(HasImage.Util.setImage())を使用します。

abstract class ListViewItemAdapter extends RefreshableArrayAdapter<Item> {
    :

    public View getView(int position, View convertView, ViewGroup parent) {
        :

        Item item = this.getItem(position);
        viewHolder.imageView.setTag(position);
        HasImage.Util.setImage(getContext(),
                                                       item.getThumbnailUrl(),
                                                       viewHolder.imageView,
        :

この二つのポイントと(後述)を実装するだけで先述の ListView の特徴がすべて実装されます。簡単でしょう?

FlowView

スクロールを検知するリスナ android.widget.AbsListView.OnScrollListener は ListView 専用です。FlowView でも使用したいので、等価な org.klab.iphoroid.widget.adpterview.OnScrollListener を実装した HasImage.AdapterViewOnScrollListener を代わりにで設定します。 # OnScrollListener は ListView 専用ではなく AdapterView に作ってくれれば全て同様に # 扱えたのに... 今から統合してもらえませんかね? > Google 様 Adapter は何を使用してもらっても結構です。イメージをを扱う場合は ListView 同様 getView() メソッド内で HasImage.Util.setImage() メソッドを用いてイメージを設定してください。

Coverflow

スクロールを検知するリスナは FlowView と同じく HasImage.AdapterViewOnScrollListener をで設定します。 Adapter は CoverFlowImageAdapterBase を extends したクラスを使用します。イメージの設定は他と同じく HasImage.Util.setImage() メソッドを用います。

    :
    coverFlow.setAdapter(new CoverFlowImageAdapter(
        CoverFlowActivity.this, items, 300, 450, true));


public class CoverFlowImageAdapter extends CoverFlowImageAdapterBase<Item> {

    public CoverFlowImageAdapter(Context context,
                                                                 List items,
                                                                 int width,
                                                                 int height,
                                                                 boolean isUserEffect) {
        super(context, items, width, height, isUserEffect);
    }

    public View getView(int position, View convertView, ViewGroup parent) {
        ImageView view = new ImageView(this.mContext);

        String url = items.get(position).getImageUrl();
        HasImage.Util.setImage(mContext,
                                                       url,
                                                       view,
            :

GridView

android.widget.GridView は AbsListView.OnScrollListener が設定できるので HasImage.ListViewOnScrollListener のインスタンスを設定してやります。あとは参照のこと。

最後に KLab iPhoroid UI 共通のコーディングとして、フリックでのスクロール中にデータロードをしない機能と、キャッシュのメモリ管理の機能を組み込むために以下のようなコードを KLab iPhoroid UI を使用する Activity に組み込みます。まず Activity には HasImage を implement します。そして OnScrollListener をフィールドに設定して、getScrollState() メソッドでそれを返すように実装します。次にその OnScrollListener を対象の KLab iPhoroid UI に設定します。最後にActivity ライフサイクルに画像、メモリ管理メソッド群(HasImage.Util.onXXX())を実装します。

public class ListViewActivity extends Activity implements HasImage {

    private HasImage.ListViewOnScrollListener onScrollListener;

    public int getScrollState() {
        return onScrollListener.getScrollState();
    }

    :

    public void onCreate(Bundle savedInstanceState) {
        :

        this.listView = (ListView) findViewById(R.id.listView);
        this.onScrollListener = new HasImage.ListViewOnScrollListener();
        listView.setOnScrollListener(onScrollListener);

    :

    protected void onResume() {
        super.onResume();

        HasImage.Util.onResume(this);
    }

    protected void onPause() {
        super.onPause();

        HasImage.Util.onPause(this);
    }

    protected void onDestroy() {
        super.onDestroy();

        HasImage.Util.onDestroy(this);
    }

さらなる詳細は配布物に含まれているデモアプリのコードを参照してください。 オープンソースで公開します KLab iPhoroid UI のコンポーネント群とそれを使用したデモプログラムが含まれています。 配布先は、 https://www.klab.jp/iphoroid/download.html になります。 デモの動画はこちら、


最後に まず KLab iPhoroid UI のベースになったいくつかのオープンソースコードの作者に感謝します。配布物の中にリストアップされています。 次にの部分はほぼ固定のコードですので毎回書くのが冗長ですよね。アノテーションとかうまく使って隠蔽できればと計画中です。 もう一つ、試しに


一つ前の記事
で人任せにしてた自動着色プログラムを KLab iPhoroid UI を使用して作ってみました。いろいろ超手抜きなんですが、それでも1時間ほど(それも大半は自動着色ロジックの移植と画像の縮小ロジック作成にかかった時間です)でコミックビューアが出来上がってしましました。 追記 2012-02-23 ダウンロードのページにデモのリポジトリのURLを記述していなかったため追記しました。