Android、Java、Web系、Linux、マラソン等の備忘録

2013/08/05

android ListView上にポップアップメニューとUIの葛藤と失敗

0 件のコメント
同じような失敗を繰り返さないよう書き留めておきます。

やりたい事

適当なUIのサンプルがなかったのですが...
こういうドロップダウンなポップアップメニューを、


ListViewの各行に設置して、その行のアイコンをタッチすると表示されるようにしたい。表示するメニューの項目は削除、編集(タイトル変更)という操作など。

このようなポップアップメニューを各行に設けることで、その行に対する操作であることが直感的で分かりやすいのではと思ったからです。
以下、抽象的な話になりますが、
行に対する操作なら、次の点も考えられます。
  • 行をタッチ → 操作用のActivityを起動
    • →そのActivity内に削除・編集のトリガーを設ける
    • →もしくはそのActivityのメニューに削除・編集トリガーを設ける
  • ListViewの子要素に削除・編集トリガを設ける
既に行をタッチでActivityを起動するようにしており、そこにListViewの行の操作をもってくるのは誤解を招きそう。またListViewの子要素は見た目も素直で良いのですが、子要素で操作というのは違和感があるので、どうにかポップアップメニューで考えてみたいと思います。

どうやって実装?

AndroidにPopupMenuというのがあって、これを実現するのに良さそうなのですが、
・PopupMenu | Android Developers
http://developer.android.com/reference/android/widget/PopupMenu.html
APIがLevel11以上....

もうそれなら、Level11(バージョン3.0)以前は利用できないアプリとして考えたのですが、このブログ書いてる最近の記事の利用状況では、
・Google、Androidバージョン別シェアでJelly Beanが40.5%の大台に到達 | 携帯 | マイナビニュース
http://news.mynavi.jp/news/2013/08/04/008/index.html
Level15以上(バージョン4.x系)のシェアが増えつつあるものの、Level10(バージョン2.3.3 - 2.3.7)も未だに33%もあり、また、この1つの機能のために、この33%の方々にゴメンネーするのは気が引け、何かいい方法はないかと模索したところ、似たようなSpinnerで実装してみることにしました。

間違ったSpinnerの使い方

とりあえず、ListView上のSpinnerがダメな理由について挙動を確認するために適当に書いたソースがあるので説明のために載せておきます。

activity_list_view_spinner.xml
Listviewを設置しているだけxml

activity_list_view_spinner_row.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/lytRow"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="10dp" >

   <LinearLayout
        android:id="@+id/lytRowItem"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal" >

        <TextView
            android:id="@+id/textView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:paddingLeft="5dp"
            android:text="TextView" />
    </LinearLayout>

    <Spinner
        android:id="@+id/spinnerRowListMenu"
        android:layout_width="50dp"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true" />

</RelativeLayout>

ListViewSpinnerActivity.java
package net.takaiwa.takenokolocalproto;

import net.takaiwa.takenokolocalproto.R;

import java.util.ArrayList;
import java.util.List;

import android.os.Bundle;
import android.app.Activity;
import android.content.Context;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.AdapterView.OnItemClickListener;

public class ListViewSpinnerActivity extends Activity {

    private static final String TAG
        = ListViewSpinnerActivity.class.getSimpleName();

    private LayoutInflater mInflater = null;
    private ListView mListView = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_list_view_spinner);

        mInflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);

        List<ListData> list_data = new ArrayList<ListData>();

        ListData data = new ListData();
        data.name = "hoge1";
        list_data.add(data);

        data = new ListData();
        data.name = "hoge2";
        list_data.add(data);

        ListAdapter list_adapter = new ListAdapter(this, list_data);
        mListView = (ListView)findViewById(R.id.listView1);
        mListView.setAdapter(list_adapter);

    }

    private static class ListData {
        public String name;
    }

    private class ListAdapter extends ArrayAdapter<ListData> {

        public ListAdapter(Context context, List<ListData> objects) {
            super(context, 0, objects);
        }

        public View getView(final int position, View convertView, ViewGroup parent) {
            final ListData list_data = this.getItem(position);

            if(null == convertView) {
                convertView = mInflater.inflate(
                        R.layout.activity_list_view_spinner_row, null);
            }

            if(null != list_data) {

                TextView text_view =
                    (TextView)convertView.findViewById(R.id.textView);
                text_view.setText(list_data.name);

                Spinner sp = (Spinner)convertView.findViewById(
                        R.id.spinnerRowListMenu);

                ArrayAdapter<String> adapter = new ArrayAdapter<String>(
                        ListViewSpinnerActivity.this,
                        android.R.layout.simple_spinner_item);
                adapter.setDropDownViewResource(
                        android.R.layout.simple_spinner_dropdown_item);
                adapter.add("編集");
                adapter.add("削除");
                sp.setAdapter(adapter);
                // これのsetOnItemClickListener()実装すると、
                // java.lang.RuntimeException:
                // setOnItemClickListener cannot be used with a spinner.
                // のExceptionが出る
//                sp.setOnItemClickListener(new AdapterView.OnItemClickListener(){
//                    @Override
//                    public void onItemClick(AdapterView<?> parent, View view, int position,
//                            long id) {
//                        Log.v(TAG, "position:" + position);
//
//                    }
//                });

                sp.setOnItemSelectedListener(new OnItemSelectedListener() {
                    @Override
                    public void onItemSelected(AdapterView<?> parent,
                            View view, int position, long id) {
                        Log.v(TAG, "position:" + position);
                    }

                    @Override
                    public void onNothingSelected(AdapterView<?> parent) {

                    }
                });
            }
            return convertView;
        }
    }
}

ListViewの各行に、TextViewとSpinnerを設置したactivity_list_view_spinner_row.xmlを動的に設定してやるサンプルです。起動すると、このようなActivityが表示されます。



ArrayAdapterを継承したクラスの、getView()メソッドでSpinnerに対するイベントを設定しています。ググると、Spinnerが選択された時のイベントはsetOnItemSelectedListenerで取得するのが一般的のようです。でも、これは、デフォルト表示されているものを選択してもonItemSelected()は呼ばれません。(※この場合「編集」をクリックしても反応はありません)
また、ListViewが生成されたタイミングで、項目数分onItemSelectedが呼ばれます。どこかで勝手にイベントを叩いてるのでしょうか、こちらにも同じような症状が書かれてます。
・Spinnerを含むViewをListViewに設定した時の問題 - Google グループ
https://groups.google.com/forum/#!topic/android-group-japan/cnrGn5WwLic
ならば、(サンプルソースではコメントアウトしてますが)setOnItemClickListenerかと思って、実装すると起動の時点でExceptionが出ます。どうやらListViewと一緒に使ってはダメのようです。他にイベントはないかと、ドキュメントを見るも見つけることができませんでした。まあ、上記が示してる通り、選択が目的のwidgetであって、クリックされた項目に対するアクションには向いてないのは当たり前ですね...

とりあえず結論

Spinnerはメニューとして使えませんよということがわかったろこで....
API Level11以前に対して、PopupMenuはどうしたら良いかググったら、

  • そもそもAPI Level11以前に対してPopupMenuを考えるな
  • PopupMenuをダイアログなどで代用した方が良いのでは?
という意見がありました。とりあえず実装せずに、先にググるべきでしたね。考えられる結論は以下ですかね。
  • やっぱりListViewの子要素で操作できるようにする
  • ポップアップの代わりにダイアログで代用
  • API Level11以上でアプリリリースで考える
  • 独自PopupMenu
  • API Levelを判定して、11以前→ダイアログ、11以上はPopupMenuの実装

主要ではない1機能のために、アプリをAPI Level11以上に引き上げてリリースするのは気が引ける中、手がかからないのが、2番目のダイアログで代用でしょう。でも、ダイアログというのは、行に対する操作を選択させるものとしてちょっと挙動に違和感があります(個人的に)。とは言え、独自PopupMenuを作るのも変にバグを埋め込みそうだし、で、結論は11以前→ダイアログ、11以上はPopupMenuの実装ですかね。


PR:スマートフォンのためのUIデザイン ユーザー体験に大切なルールとパターン
PR:スマートフォンサイトUI/UXデザイン実践テクニック ~理想的なユーザーエクスペリエンス実現のために~

0 件のコメント :

コメントを投稿