スキップしてメイン コンテンツに移動

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

同じような失敗を繰り返さないよう書き留めておきます。

やりたい事

適当な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デザイン実践テクニック ~理想的なユーザーエクスペリエンス実現のために~

コメント

このブログの人気の投稿

Javaでprivateなfieldやmethodにアクセスする

JUnitでテストしてると、privateなフィールドにアクセスして、値を参照したりセットしたりしたくなるわけですが、よく使うのでメモしておきます。 例えば、次のような対象のクラスがあるとします。 public class ParentClass { private String hoge = "ParentClass!!"; public void dispMsg() { System.out.println("dispMsg:" + hoge); } private void privateDispMsg(String msg) { System.out.println("dispMsg:" + msg); } }

GolangでWindows GUIアプリケーション

GUIアプリ作成の前提 社内ツールとしてexeで配布 開発環境はGoLandを使う 社内ツールとしてexeを配布ということであれば、Visual StudioでC#による開発だと思います。しかしながら、Go言語を習得したいのと、GoLandの補完機能が便利で、Android Studio使っていたこともあり、とっつきやすいという点からGo縛りでGUIアプリケーションを考えたいと思います。 lxn/walk Windows application library kit for Go. Windows向けしか考えていないので、まずこのライブラリなのですが、ボタンやコンボボックスが思ったように並ばなかったり、手軽にイメージボタンを配置したりなど、簡単にレイアウトを変更できない課題に直面しました。レイアウト作成だけで時間を取られてしまいます。そう言えば、Androidアプリ開発のときは、XMLでデザイン部分を切り離してたのを思い出して、今回の調査の運びとなりました。 fyne-io/fyne Cross platform native GUIs designed for Go based on Material Design. Supports: Linux, macOS, Windows, BSD, iOS and Android. walkと比べると、クロスプラットフォームで作成できるのですが、こちらもコードの中にデザインを書いていく形でした。 therecipe/qt therecipe/qt allows you to write Qt applications entirely in Go or JavaScript. Qt Creatorなるものがあるようで、デザイン部分を切り離せる印象はあるものの、最新のPCでもビルドに時間がかかるようで、学習コストもかかるという記事を見かけて断念しました。 どうしたものかとツイートしましたら、下記のようにご助言をいただいた。 同じ課題に対し色々試した結果、PWAのフルキャッシュドに落ち着いた。中身はGo+WASMで。 https://t.co/e60whDTV16 — のぼのぼ📡 (@nobonobo) July 21, 2020 PWA 早速調べてみました。Googleが進めているプロジェクトで、ネイティブアプリのよ

Eclipseの高速化メモ

Eclipseが重いと一言に言っても、いろいろな工程での話があると思いますが、過去記事のなども含めてこの記事にピックアップしておきたいと思います。以下はWindows環境での話です。