スポンサーリンク

AndroidのListViewを,いかにシンプルかつ楽にコーディングするか。Adapterを記述不要のライブラリ案


ListViewを楽に使うためのDSLを考案する。

はじめに

AndroidのUIで,ListViewの実装はめんどい。


スマホアプリで,要素がリスト形式に並ぶ,というシーンは頻繁にある。

だが特にAdapter周りを毎回コーディングするのが面倒で,どこか回りくどい。

データ構造も微妙に煩雑。

AndroidアプリでListViewをカスタマイズし,Web上の画像を行ごとに表示するサンプルコード (SimpleAdapterクラスを独自に拡張)
http://language-and-engineering.hatenablog.jp/entry/20111014/p1


この手間を,いかに省くか。


一つの方法は,そもそもListViewを使わないことだ。

別の手段で,複数の要素を「動的に」列挙する。

この方法については,Android-MVCフレームワークの初版公開時にすでに提案してある。

Androidアプリの画面レイアウトを,まるでjQueryのようなコードで動的構築できるライブラリ (の試作品。UIコーディングのためのDSL)
http://language-and-engineering.hatenablog.jp/entry/20120210/p1


forループ中で要素を動的にaddしているサンプルコード
http://code.google.com/p/android-mvc-...

そして,今回提案するもう一つの方法は,ListViewを手軽に扱うためのDSLを構築することだ。



上図のような単純なUIを実現するために,

準備済みのライブラリがあれば,どれぐらい短くコーディングできるのか。

サンプルコード

Activityのコード:

package com.example;

import com.android_mvc.framework.ui.UIBuilder;
import com.android_mvc.framework.ui.list.ListViewDataStore;
import com.android_mvc.framework.ui.list.LineUIBuilder;
import com.android_mvc.framework.ui.list.MListView;
import com.android_mvc.framework.ui.view.MButton;
import com.android_mvc.framework.ui.view.MLinearLayout;

import android.os.Bundle;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.TextView;

/**
 * ListViewのDSLのサンプル画面。
 * @author id:language_and_engineering
 *
 */
public class ListViewSampleActivity extends Activity {

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

    final Context context = this;

    new UIBuilder(this)
      .withoutScroll()
      .add(

        new MButton(this)
          .text("ボタン1")
        ,

        new MLinearLayout(this)
          .heightPx(350)
          .add(

            // ここからListView
            new MListView(this)
              .bgcolor(Color.parseColor("#ffffff"))
              .widthFillParent()
              .defineLines(

                // 行のUIを定義
                new LineUIBuilder(R.layout.menu_listline)
                {
                  @Override
                  protected void describeOneLine( final int position, View lineView )
                  {
                    // 動的に文言を表示
                    ((TextView)lineView.findViewById(R.id.tv1)).setText(
                      (position + 1)
                      + ": "
                      + getLineData(position, "word_title").toString()
                    );

                    // 行タップ時の挙動
                    lineView.setOnClickListener(new OnClickListener(){
                      @Override
                      public void onClick(View v) {
                        // 指定されたアクティビティに画面遷移
                        context.startActivity( new Intent(
                          context, (Class<?>) getLineData(position, "dest")
                        ) );
                      }
                    });

                  }
                }
                ,

                // 行内のデータを定義
                new ListViewDataStore()
                  .newLine()
                    .put("word_title", "日本語")
                    .put("dest", AActivity.class)
                  .newLine()
                    .put("word_title", "英語")
                    .put("dest", BActivity.class)
                  .newLine()
                    .put("word_title", "ンデベレ語")
                    .put("dest", CActivity.class)
                  .newLine()
                    .put("word_title", "C言語")
                    .put("dest", DActivity.class)
              )

          ) // ListView ここまで

          ,
          new MButton(this)
            .text("ボタン2")

      )
    .display();

  }

}


リストビューの1行分のレイアウトXML:

menu_listline.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/linearLayout1"
  android:layout_width="fill_parent"
  android:layout_height="wrap_content"
  android:orientation="horizontal"

  android:padding="20dp"
  android:background="#ffffff"
>

  <TextView
    android:id="@+id/tv1"
    android:textAppearance="?android:attr/textAppearanceLarge"
    android:layout_height="wrap_content"
    android:layout_width="wrap_content"

    android:textColor="#222222"
  />


  <ImageView
    android:layout_alignParentRight="true"

    android:layout_height="30dp"
    android:layout_width="30dp"

    android:layout_marginRight="20dp"

    android:src="@android:drawable/ic_media_play"
  />

</RelativeLayout>


ソースコードが,画面の仕様をそのまま表現している。


1行分のUI定義と,各行に渡したいデータとを記述するだけ。

付随的なことは一切,コードに現れない。



※各行内のレイアウトを動的に構築するという発想もあったのだが,没とした。

ListViewの性質上,各行の描画処理(getView)が何度も繰り返し頻繁にコールされるので,

その度に見た目を1行ずつ動的に構築する方法には無理があったため。

なので,ベースとなる行のレイアウトはXMLで定義する必要がある。


サンプルコードを動作させるための,ライブラリ側のコード

以下はフレームワーク側のコードになる。

Android-MVC framework ver0.3に機能追加する形での提案となる。

わりと簡単な事しかやっていない。



上述のサンプルコード中で,

LineUIBuilderは,単なるメソッドのラッパ。

ListViewDataStoreは,ArrayListとHashMapのラッパ。


残りのクラスが本質的であり,その中でも要点となる部分の処理を掲載する。


まず,ビュー部品のラッパ。

MListView.java

package com.android_mvc.framework.ui.list;

import java.util.HashMap;
import java.util.List;

import com.android_mvc.framework.annotations.SuppressDebugLog;
import com.android_mvc.framework.ui.view.IFWView;
import com.kids_be_genius.EnglishGreetingVoice.util.Util;


import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ListView;
import android.widget.SimpleAdapter;

/**
 * ListViewのラッパークラス。
 * @author id:language_and_engineering
 *
 */
@SuppressDebugLog(true)
public class MListView extends ListView implements IFWView
{

    // NOTE:
    // ・ScrollViewの中では使わないこと。高さがおかしくなる。
    //   http://stackoverflow.com/questions/10709411/android-listview-rows-in-scrollview-not-fully-displayed-clipped
    // ・高さを変えるには,ラッパーViewとしてLinearLayoutを設け,そこでheightを指定すること。

    // ListViewとアダプタの基本的な使い方 @see http://language-and-engineering.hatenablog.jp/entry/20111014/p1


〜中略(属性操作など)〜


    // 行の定義

    /**
     * 行のUIとデータを定義
     */
    public MListView defineLines(LineUIBuilder line_builder, ListViewDataStore listViewDataStore) {

        // アダプタを作ってセットする
        this.setAdapter(
            new MSimpleAdapter(
                context,
                listViewDataStore.getResult(),
                line_builder.line_template_layout_id,
                new String[]{},
                new int[]{},
                line_builder
            )
        );

        return this;
    }

    /**
     * アダプタ
     */
    protected class MSimpleAdapter extends SimpleAdapter
    {
        private LayoutInflater mInflater;
        private LineUIBuilder line_builder;


        // 初期化
        public MSimpleAdapter (
            Context context,
            List<HashMap<String, Object>> list_data,
            int resource,
            String[] from,
            int[] to,
            LineUIBuilder line_builder
        )
        {
            super(context, list_data, resource, from, to);

            this.line_builder = line_builder;
            this.line_builder.registerData(list_data);

            // リストの動的な描画のためにインフレータを生成
            this.mInflater = (LayoutInflater) context.getSystemService( Context.LAYOUT_INFLATER_SERVICE );

            Util.d("ListView:アダプタの初期化が完了");
        }


        // 1行を描画するたびに呼ばれるメソッド
        @Override
        public View getView(int position, View convertView, ViewGroup parent) {

            // 行を表すビュー
            View v = convertView;
            if(v == null){
                v = mInflater.inflate(line_builder.line_template_layout_id, null);
            }

            // この行のUIを構築
            //Util.d("ListView:" + position + "行目の描画を開始");
            line_builder.describeOneLine(position, v);
            //Util.d("ListView:" + position + "行目の描画を完了");

            return v;
        }

    }

}


また,UIBuilder.javaに,少し追記する。

    /**
     * ルートビューからスクロールをなくす
     */
    public UIBuilder withoutScroll()
    {
        // ScrollViewではないレイアウトを雛形として使う
        context.setContentView(R.layout.fw_template_noscroll);
        this.rootLayout = (MLinearLayout)context.findViewById(R.id._FWRootLayout);

        // NOTE: setContentViewを複数回読んだら,後者が優先される
        // http://ichitcltk.hustle.ne.jp/gudon/modules/pico_rd/index.php?content_id=44

        return this;
    }


そして,スクロールのないレイアウトXMLを定義。

(ListViewにはスクロール機能がついているので,ScrollViewの中に配置できないから)

fw_template_noscroll.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">

<com.android_mvc.framework.ui.view.MLinearLayout
    android:id="@+id/_FWRootLayout"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">


    <!-- ここに,UIBuilderでViewが構築される。 -->


</com.android_mvc.framework.ui.view.MLinearLayout>
</LinearLayout>


これでOK。

無いよりはずっとましだろう。

補足

ンデベレ語について:

しりとりで勝つ
http://www.eonet.ne.jp/~arawashi/ur/h...

  • 「ン」でしりとりに勝つには、アフリカの力を借りるのが最もスマート  ※「ンジャメナ」など
  • ジンバブエには大きくわけて二つの民族が住んでいます。ひとつはジンバブエの首都ハラレを中心としたショナ族、そしてもう一つがジンバブエ第二の都市ブラワヨ周辺の民族、ンデベレ族


関連する記事:

AndroidのUIで,レイアウトXMLの記述を簡素にするための,7つの基礎知識
http://language-and-engineering.hatenablog.jp/entry/20121114/SimpleAndroidLay...


jQuery Mobile と HTML5 で、Androidのネイティブアプリを作成する手順
http://language-and-engineering.hatenablog.jp/entry/20120717/CreateAndroidNat...


Android SDK の動かないコード(中級編) ListView内の要素にアクセスしようとするとNullPointerExceptionで落ちるエラー
http://language-and-engineering.hatenablog.jp/entry/20111013/p1