ごんれのラボ

iOS、Android、Adobe系ソフトの自動化スクリプトのことを書き連ねています。

SingleChoiceItems をカスタマイズして任意の要素を disable にできる DialogFragment を作った

概要

SingleChoiceItems をカスタマイズして、特定の条件のときに任意の要素を disable にできるリストを内包したダイアログを実装したので、公開しました

経緯

案件で「特定の条件のときにリストの要素を disable にしてユーザーが選択できないようにするダイアログ」が必要になりました。
よくある要件に思えたのでググってみたんですが、意外なことにいいサンプルが見つからなかったので、自分で実装してみました。

ソースコード

CustomSingleChoiceItemDialogFragment

SingleChoiceItems を内包した DialogFragment です。
以下にソースコードを転記して、要所だけコメントを追記しました。

package com.macneko.customsinglechoiceitemdialog.view

import android.app.AlertDialog
import android.os.Bundle
import androidx.fragment.app.DialogFragment
import com.macneko.customsinglechoiceitemdialog.adapter.CustomAdapter

class CustomSingleChoiceItemDialogFragment : DialogFragment() {
  private lateinit var title: String // ダイアログのタイトル
  private lateinit var entries: List<String> // リストに表示する配列
  private var entryIndex = 0 // リストで選択状態にする index。`-1` で選択なし
  private var disableIndex = -1 // リストで disable 状態にする index。`-1` で disable なし
  private lateinit var adapter: CustomAdapter // Adapter
  private lateinit var requestKey: String // 呼び出し元が `setFragmentResultListener` で選択結果を受け取るときの Key

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    arguments?.run {
      title = getString(PARAM_TITLE) ?: throw IllegalArgumentException("arg is null")
      entries = getStringArrayList(PARAM_ENTRIES) ?: throw IllegalArgumentException(
        "arg is null"
      )
      entryIndex = getInt(PARAM_ENTRY_INDEX)
      disableIndex = getInt(PARAM_DISABLE_INDEX, -1)
      requestKey = getString(PARAM_REQUEST_KEY) ?: throw IllegalArgumentException("arg is null")
    }

    adapter = CustomAdapter(entries, disableIndex)
  }

  override fun onCreateDialog(savedInstanceState: Bundle?) =
    AlertDialog.Builder(context).apply {
      setTitle(title)
      setSingleChoiceItems(adapter, entryIndex) { _, selectedIndex ->
        val bundle = Bundle().apply {
          putInt(RESULT_KEY, selectedIndex) // 選択した index を bundle に詰める
        }
        parentFragmentManager.setFragmentResult(requestKey, bundle) // `setFragmentResult` で結果を返す
        dismiss()
      }
      setNegativeButton(android.R.string.cancel) { _, _ ->
        dismiss()
      }
    }.create() ?: super.onCreateDialog(savedInstanceState)

  companion object {
    private const val PARAM_TITLE = "param_title"
    private const val PARAM_ENTRIES = "param_entries"
    private const val PARAM_ENTRY_INDEX = "param_entry_index"
    private const val PARAM_DISABLE_INDEX = "param_disable_index"
    private const val PARAM_REQUEST_KEY = "PARAM_REQUEST_KEY"
    const val RESULT_KEY = "result_key"

    fun newInstance(
      title: String,
      entries: List<String>,
      entryIndex: Int,
      disableIndex: Int = -1,
      requestKey: String
    ) =
      CustomSingleChoiceItemDialogFragment().apply {
        arguments = Bundle().apply {
          putString(PARAM_TITLE, title)
          putStringArrayList(PARAM_ENTRIES, ArrayList(entries))
          putInt(PARAM_ENTRY_INDEX, entryIndex)
          putInt(PARAM_DISABLE_INDEX, disableIndex)
          putString(PARAM_REQUEST_KEY, requestKey)
        }
      }
  }
}

CustomAdapter

特定の要素を disable にする BaseAdapter のサブクラスです。
以下にソースコードを転記して、要所だけコメントを追記しました。

package com.macneko.customsinglechoiceitemdialog.adapter

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.TextView

class CustomAdapter(
  private val items: List<String>,
  private val disableIndex: Int // コンストラクタで disable にする index を受け取る
) : BaseAdapter() {
  override fun getCount() = items.size

  override fun getItem(position: Int) = items[position]

  override fun getItemId(position: Int): Long = 0

  override fun isEnabled(position: Int) = position != disableIndex 

  override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
    val view: View
    val holder: ViewHolder
    if (convertView == null) {
      view = LayoutInflater.from(parent?.context)
        .inflate(android.R.layout.simple_list_item_single_choice, parent, false) // `android.R.layout.simple_list_item_single_choice` は SimpleChoiceItems のレイアウトXML
      holder = ViewHolder()
      holder.textView = view.findViewById<View>(android.R.id.text1) as TextView
      view.tag = holder
    } else {
      view = convertView
      holder = view.tag as ViewHolder
    }
    setViewItems(position, holder)
    return view
  }

  private fun setViewItems(position: Int, holder: ViewHolder) {
    holder.textView?.apply {
      text = getItem(position)
      // disableIndex と一致したら `isEnabled = false` になる
      isEnabled = isEnabled(position)
    }
  }

  private class ViewHolder {
    var textView: TextView? = null
  }
}

プロジェクトへの導入方法

  1. CustomSingleChoiceItemDialogFragmentCustomAdapter をプロジェクトに追加する
  2. Activity などのダイアログを表示するクラスにダイアログを表示するコードを書く
val dialog = CustomSingleChoiceItemDialogFragment.newInstance(
    "title", // ダイアログのタイトル
    ["First", "Second", "Third"], // リストに表示する値の配列
    0, // リストで選択状態にする index
    1, // リストで disable 状態にする index。`-1` で disable なし
    "REQUEST_KEY_CUSTOM" // setFragmentResultListener で選択結果を受け取るときの Key
)
dialog.show(supportFragmentManager, "CustomSingleChoiceItemDialogFragment")
  1. 2 のクラスの onCreate にダイアログの選択結果を受け取るコードを書く
supportFragmentManager.setFragmentResultListener("REQUEST_KEY_CUSTOM", this) { _, bundle ->
      Log.d("sample", bundle.getInt(CustomSingleChoiceItemDialogFragment.RESULT_KEY, -1)) // bundle に選択した index が入っているので、取り出してログに出力する
}

サンプルアプリケーション

ソースコード

以下のリポジトリで公開しています
https://github.com/macneko-ayu/CustomSingleChoiceItemDialog

デモ動画

画面の説明

合番 説明
1 4 をタップして表示されたダイアログで選択した要素を表示します
2 disable にする要素を選択するダイアログを表示します
3 2 で disable にした要素の position を表示します
4 2 で選択した要素を disable にしたダイアログを表示します

実装してみて思ったこと

今回初めて使った SingleChoiceItems ですが、簡単にリスト選択型のダイアログが作れて便利ですね。