【Android】RecyclerViewのセルにポップアップメニューを実装

# はじめに
* 動作確認したAPI Versionは21〜23
* サポートライブラリ v7を使用

# 作るもの

Material Designのこれ

![スクリーンショット 0028-03-24 12.05.22.png](https://qiita-image-store.s3.amazonaws.com/0/87641/38088234-b8c4-59b7-7c1b-64bf6a09a07e.png)

# 実装方法

PopupWindow(API Level1から存在)を使うと簡単でした。
Design Support Libraryとかに目が行きがちですが、昔からあるクラスで今風に実装できることも多いです。

## RecyclerViewを作る

RecyclerViewの基本的な作り方は以下を参照してください。
[【Android】RecyclerViewの基本的な実装](http://qiita.com/HideMatsu/items/a9ab48608e4f681d31fe)

まずはこんな感じでRecyclerAdapterを用意しましょう

```java:MenuRecyclerAdapter.java
public class MenuRecyclerAdapter extends RecyclerView.Adapter<MenuRecyclerAdapter.ViewHolder> {

    /* ---------------------------------------------------------------------- */
    /* Field                                                                  */
    /* ---------------------------------------------------------------------- */

    // クラスタグ
    public static final String TAG = MenuRecyclerAdapter.class.getSimpleName();

    // 表示データのリスト
    private ArrayList<String> dataList;

    // コンテキスト
    private Context context;

    // RecyclerViewのアイテムクリックリスナー
    private OnRecyclerListener recyclerListener;

    /* ---------------------------------------------------------------------- */
    /* Listener                                                               */
    /* ---------------------------------------------------------------------- */

    /**
     * RecyclerViewのアイテムクリックリスナー
     */
    public interface OnRecyclerListener {
        void onItemClicked(int position, String data);
        void onMenuItemClicked(int position, String data, int menuId);
    }

    /* ---------------------------------------------------------------------- */
    /* Method                                                                 */
    /* ---------------------------------------------------------------------- */

    /**
     * コンストラクタ
     */
    public MenuRecyclerAdapter(Context context, ArrayList<String> data, OnRecyclerListener listener) {
        this.context = context;
        this.dataList = data;
        this.recyclerListener = listener;
    }

    /**
     * ViewHolderの生成
     */
    @Override
    public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
        // 表示するレイアウトを設定
        return new ViewHolder(LayoutInflater.from(context).inflate(R.layout.list_item_recycler_menu, viewGroup, false), context, recyclerListener);
    }

    /**
     * ViewHolderへのデータセット
     */
    @Override
    public void onBindViewHolder(ViewHolder viewHolder, final int i) {
        // データセット
        viewHolder.position = i;
        viewHolder.data = dataList.get(i);

        // セルのクリック処理
        viewHolder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // セルの番号とデータを返す
                recyclerListener.onItemClicked(i, dataList.get(i));
            }
        });
    }

    /**
     * リスト数を返す
     */
    @Override
    public int getItemCount() {
        if (dataList != null) {
            return dataList.size();
        } else {
            return 0;
        }
    }

...

```

Adapterに特に変わった所はありません。
メニュー項目のクリックイベントを、RecyclerViewのクリックイベントリスナーについでに持たせてるくらいです。(onMenuItemClicked)

一応、Fragment側ではこんな感じで受け取る準備をしておきます。

```java:RecyclerFragment.java
public class RecyclerFragment extends Fragment implements MenuRecyclerAdapter.OnRecyclerListener {

...

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

        // 適当にデータ作成
        ArrayList<String> array = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            array.add("");
        }

        mAdapter = new MenuRecyclerAdapter(mActivity, array, this);
        mRecyclerView.setAdapter(mAdapter);
    }

...

    /* ---------------------------------------------------------------------- */
    /* OnRecyclerListener                                                     */
    /* ---------------------------------------------------------------------- */
    @Override
    public void onItemClicked(int position, String data) {
        String message = String.format("Position:%d Data:%s", position, data);
        Toast.makeText(mActivity, message, Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onMenuItemClicked(int position, String data, int menuId) {
        String message = String.format("Position:%d MenuItem:%d", position, menuId);
        Toast.makeText(mActivity, message, Toast.LENGTH_SHORT).show();
    }

...

}
```

とりあえず押された物の情報をトースト表示するようにしています。


## RecyclerViewのセルのレイアウトを作成

セルはとりあえずこんな感じです。

```xml:list_item_recycler_menu.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="match_parent"
              android:layout_height="160dp">
    <!-- セルに表示する画像(システムアイコン) -->
    <ImageView
        android:src="@android:drawable/sym_def_app_icon"
        android:background="#4CAF50"
        android:tint="#e91e63"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

    <!-- メニューを開くボタン(黒画像しか手元に無かったのでtintで白くしましたすみません) -->
    <ImageButton
        android:id="@+id/menu_button"
        android:tint="@color/white"
        android:src="@drawable/ic_more_vert_black_24dp"
        android:background="@drawable/selectable_button_default"
        android:layout_gravity="top|right"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="8dp"
        android:padding="8dp"/>

    <!-- 区切り線的なもの -->
    <View
        android:layout_height="1dp"
        android:layout_width="match_parent"
        android:background="@android:color/white"
        android:layout_gravity="bottom"/>
</FrameLayout>
```

そうするとこんな感じのキモいリストが作れます。

![PopupWindow_SS_mini.png](https://qiita-image-store.s3.amazonaws.com/0/87641/618dcbaa-1e7e-17c4-c53b-bd17dbb44b25.png)

## ContextMenuのレイアウトを作成

表示したいメニューのレイアウトファイルを(今回は雰囲気で)作成します。
このレイアウトファイルの一番親でlayout_widthを設定しても、後で生成時にコード上で設定しないと無視されます。その為、基本的には一番親はwrap_contentです。

```xml:menu.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical" android:layout_width="wrap_content"
              android:layout_height="wrap_content"
    android:background="@android:color/transparent">
    <LinearLayout
        android:orientation="vertical" android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@drawable/menu_background"
        android:layout_marginEnd="8dp"
        android:layout_marginBottom="8dp"
        android:elevation="8dp"
        android:paddingTop="8dp"
        android:paddingBottom="8dp">

        <Button
            android:id="@+id/menu1"
            android:text="Item1"
            android:background="@drawable/selectable_button_default"
            android:paddingLeft="16dp"
            android:paddingRight="16dp"
            android:layout_width="match_parent"
            android:layout_height="56dp" />

        <Button
            android:id="@+id/menu2"
            android:text="Item2"
            android:background="@drawable/selectable_button_default"
            android:paddingLeft="16dp"
            android:paddingRight="16dp"
            android:layout_width="match_parent"
            android:layout_height="56dp" />

        <Button
            android:id="@+id/menu3"
            android:text="Item3"
            android:background="@drawable/selectable_button_default"
            android:paddingLeft="16dp"
            android:paddingRight="16dp"
            android:layout_width="match_parent"
            android:layout_height="56dp" />
    </LinearLayout>
</LinearLayout>
```



menu_backgroundとselectable_button_defaultは、ただの自作drawableです

```xml:menu_background.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <solid android:color="@android:color/white"/>
    <corners android:radius="2dp"/>
</shape>
```

```xml:selectable_button_default.xml
<?xml version="1.0" encoding="utf-8"?>
<ripple
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:color="#42000000">
    <item android:id="@android:id/mask" android:drawable="@android:color/white" />
</ripple>
```

## RecyclerAdapterのViewHolderにPopupWindowの生成・表示を実装

ここでようやくPopupWindowの生成になります。
さっきのAdapterクラスのViewHolderで、PopupWindowの生成と表示を行います。

```java:MenuRecyclerAdapter.ViewHolder
    public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {

        // このセルの番号
        public int position;

        // このセルのデータ
        public String data = null;

        // メニューを表示する為のボタン(縦の・・・みたいなやつ)
        public ImageButton button = null;

        // 表示するメニュー(PopupWindow)のインスタンス
        public PopupWindow popupWindow = null;

        // Adapterから受け取ったメニューのクリックリスナー
        private OnRecyclerListener recyclerListener = null;

        // コンストラクタ
        public ViewHolder(View itemView, Context context, OnRecyclerListener recyclerListener) {
            super(itemView);
            this.recyclerListener = recyclerListener;

            // PopupWindowのインスタンスを生成
            popupWindow = new PopupWindow(context);

            // PopupWindowに表示するViewを生成
            View contentView = LayoutInflater.from(context).inflate(R.layout.menu, null);

            // クリックイベントのやりとりはお好みの方法で
            contentView.findViewById(R.id.menu1).setOnClickListener(this);
            contentView.findViewById(R.id.menu2).setOnClickListener(this);
            contentView.findViewById(R.id.menu3).setOnClickListener(this);

            // PopupWindowに表示するViewをセット
            popupWindow.setContentView(contentView);

            // PopupWindowに表示するViewのサイズを設定
            float width = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 140, context.getResources().getDisplayMetrics());
            popupWindow.setWindowLayoutMode((int) width, WindowManager.LayoutParams.WRAP_CONTENT);
            popupWindow.setWidth((int) width);

            // PopupWindowの外をタッチしたらPopupWindowが閉じるように設定
            popupWindow.setOutsideTouchable(true);

            // PopupWindow外のUIのタッチイベントが走らないようにフォーカスを持っておく
            popupWindow.setFocusable(true);

            // PopupWindow内のクリックを可能にしておく
            popupWindow.setTouchable(true);

            // レイアウトファイルで設定した背景のさらに背景(黒とか)が生成される為、ここで好みの背景を設定しておく
            popupWindow.setBackgroundDrawable(new ColorDrawable(context.getResources().getColor(android.R.color.transparent)));

            // メニューを表示するためのボタンを生成
            button = (ImageButton) itemView.findViewById(R.id.menu_button);

            // メニューを表示するためのクリックイベント設定
            button.setOnClickListener(this);
        }

        /**
         * クリックイベント
         */
        @Override
        public void onClick(View v) {
            if (v != null) {
                if (v.getId() == R.id.menu_button) {
                    // アンカーにするViewと、そのViewの左下から見た位置を指定してPopupWindowを表示する
                    popupWindow.showAsDropDown(v, v.getWidth(), -v.getHeight());
                } else {
                    // メニュー項目がクリックされたらFragmentに返して処理したりするのが多そう
                    recyclerListener.onMenuItemClicked(position, data, v.getId());
                }
            }
        }
    }
```

## 結果

![SS_3.png](https://qiita-image-store.s3.amazonaws.com/0/87641/496e9d68-d0c0-001e-7d68-f8baa5d8ae0b.png)

画面の半分を境に、自動的にアンカーから上向きか下向きの表示を切り替えてくれるのも便利です。
あとはデザインを調整して、理想のContextualMenuにしてみてください。

おわり