📝
androidのマルチ選択に全選択ボタンを追加する
TL;DR
(雑な撮影ですみません)
以下のダイアログフラグメントを追加して使い方にあるのをボタンのonClickに設定すればok。もとのフラグメントが必要なのでobserverなどを使って発火させる必要がある
class CheckBoxListDialogFragment : DialogFragment() {
override fun onCreateDialog(@Nullable savedInstanceState: Bundle?): Dialog {
val list = requireArguments().getStringArrayList(ARGS_LIST)?.toTypedArray()
val checkList = requireArguments().getBooleanArray(ARGS_CHECK_LIST)
val title = createTitleText()
val allSelectBtn = createAllSelectBtn(checkList)
val titleWrapper = createTitleWrapper(title, allSelectBtn)
val builder = AlertDialog.Builder(requireActivity()).apply {
setMultiChoiceItems(list, checkList) { dialog, which, isChecked ->
val count = (dialog as AlertDialog).listView.checkedItemCount
list?.let {
allSelectBtn.isChecked = count == list.size
}
checkList?.set(which, isChecked)
}
setPositiveButton("ok") { _, _ ->
setFragmentResult(REQUEST_KEY, bundleOf("check_list" to checkList))
}
setNegativeButton("cancel") { _, _ ->
dialog?.cancel()
}
setCustomTitle(titleWrapper)
}
return builder.create()
}
private fun createTitleText(): TextView {
return TextView(context).apply {
val lp = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT,
)
lp.setMargins(
lp.leftMargin,
UIHelper.convertDp2PxAsInt(20, context),
lp.rightMargin,
UIHelper.convertDp2PxAsInt(20, context)
)
layoutParams = lp
gravity = Gravity.CENTER
text = "Title"
setTextSize(TypedValue.COMPLEX_UNIT_SP, 20f)
}
}
private fun createAllSelectBtn(checkList: BooleanArray?): CheckBox {
return CheckBox(context).apply {
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT,
).apply {
// about checkbox and text
setMargins(
UIHelper.convertDp2PxAsInt(20, context),
topMargin,
rightMargin,
bottomMargin
)
}
// about text (except checkbox)
setPadding(
UIHelper.convertDp2PxAsInt(20, context),
paddingTop,
paddingRight,
paddingBottom
)
text = "select all"
setTextSize(TypedValue.COMPLEX_UNIT_SP, 17f)
isChecked = checkList != null && checkList.all { it }
setOnClickListener {
val isChecked = (it as CheckBox).isChecked
val listView = (dialog as (AlertDialog)).listView
checkList?.let {
for (i in checkList.indices) {
// toggle check (display)
listView.setItemChecked(i, isChecked)
// toggle check (data)
checkList[i] = isChecked
}
}
}
}
}
private fun createTitleWrapper(title: TextView, allSelectBtn: CheckBox): LinearLayout {
return LinearLayout(context).apply {
layoutParams = ViewGroup.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT,
)
orientation = LinearLayout.VERTICAL
addView(title)
addView(allSelectBtn)
}
}
companion object {
const val ARGS_LIST = "list"
const val ARGS_CHECK_LIST = "check_list"
private const val REQUEST_KEY = "request_key"
fun show(
target: Fragment,
list: ArrayList<String>,
checkList: BooleanArray,
onClickOK: (checkList: BooleanArray) -> Unit,
) {
list.size
newInstance(list, checkList).run {
target
.childFragmentManager
.setFragmentResultListener(
REQUEST_KEY,
target.viewLifecycleOwner
) { requestKey, bundle ->
if (requestKey != REQUEST_KEY) return@setFragmentResultListener
if (bundle.containsKey("check_list")) {
bundle.getBooleanArray("check_list")?.let {
onClickOK(it)
}
}
}
show(target.childFragmentManager, "CheckBoxListDialogFragment")
}
}
private fun newInstance(
list: ArrayList<String>,
checklist: BooleanArray,
): CheckBoxListDialogFragment {
val fragment = CheckBoxListDialogFragment()
val args = Bundle().apply {
putStringArrayList(ARGS_LIST, list)
putBooleanArray(ARGS_CHECK_LIST, checklist)
}
fragment.arguments = args
return fragment
}
}
}
使い方
fun onClickShow () {
val list = arrayOfNulls<String>(15).mapIndexed { index, _ -> "test${index}" }
CheckBoxListDialogFragment.show(this, ArrayList(list) , BooleanArray(list.size)) { resultBooleanArray ->
val checkedList = list.filterIndexed { index, _ -> resultBooleanArray[index]}
// do something
}
}
解説
CheckBoxListDialogFragment.showの引数について
引数 | 説明 |
---|---|
target |
呼び出し元のフラグメント |
list |
表示するテキストのリスト |
checklist |
listに対応したbooleanの配列。したがってlist.size == checklist.size である。trueのものはチェック済み、falseは未チェックである。 |
onClickOK | ダイアログをokボタンで閉じたときに発火する。項目のチェックについてのbooleanArrayが返ってくるので、大体の場合はこれを使ってチェックしたものだけを取り出す。 |
時系列順に軽く説明
-
onClickShow
がバインドされたボタンをタップするとCheckBoxListDialogFragment
のcompanion objectに記述されているshow
メソッドが発火する -
show
メソッド内部ではnewInstance
が発火して、今回の主役であるCheckBoxListDialogFragment
を作成する - 作成が終わったら次に
FragmentResultListener
を登録する。ここでonClickOKを使う。 -
FragmentResultListener
の登録が終わったらDialogFragment.show
を呼び出す。 -
show
が呼び出されたあとにonCreateDialog
が発火する。 - タイトルと全選択ボタンを作ってラッパーでまとめる。
-
AlertDialog.Builder
を作成してタイトル(6で作ったもの)、チェックリスト、okボタン、キャンセルボタンをセットする。 - okボタンでは
setFragmentResult
でcheck_list
という名前でbooleanArrayをbundleに追加。その後3で登録したlistenerが発火してコールバックを呼び出す
全選択ボタンについて詳細に説明
レイアウトの説明
setMargin
とsetPadding
で左側にそれぞれ20dpずつ設定する必要がある。
-
setMargin
ではチェックボックスとテキストの両方をまとめたコンポーネントに対してマージンを設定する。 -
setPadding
ではチェックボックスは関係なく、テキストのpaddingだけを設定する
だめ
marginだけ40dpを設定したとき
だめ
paddingだけ40dpを設定したとき
OK
両方に20dpを設定したとき
他の説明
以下で初期値を決める。全部チェックされていたら全選択ボタンもチェック、どれか1つでもチェックされていない場合は全選択ボタンもチェックされない。
isChecked = checkList != null && checkList.all { it }
以下でクリックリスナーを登録。動作はなんとなくわかると思うが実装は少し工夫する必要がある。
setOnClickListener {
val isChecked = (it as CheckBox).isChecked
val listView = (dialog as (AlertDialog)).listView
checkList?.let {
for (i in checkList.indices) {
// toggle check (display)
listView.setItemChecked(i, isChecked)
// toggle check (data)
checkList[i] = isChecked
}
}
}
ここは2つ注意点がある。
-
checkList.indices
の部分は、0..listView.size
を使ってはダメ。listView.size
は表示している数なので100個とかある場合100がほしいのに12とかになる。 - チェックの切替は表示と内部データの両方に対して行う必要がある。
チェックの切替について。
両方行わないと見た目と中身が違ってくるので、全部チェックしたのに何も変わってない、などが起こってしまう。
// ここではチェックの見た目をトグルしている
listView.setItemChecked(i, isChecked)
// ここでは内部データのチェックをトグルしている
checkList[i] = isChecked
onCreateDialogのbuilderのところでも全選択ボタンについて書いている。
これはそれぞれをタップして全部チェックしたときに全選択ボタンもチェックされるようにするもの。
逆に全部チェックした状態から1つだけチェックを外すと全選択ボタンのチェックも外れる。
setMultiChoiceItems(list, checkList) { dialog, which, isChecked ->
val count = (dialog as AlertDialog).listView.checkedItemCount
list?.let {
allSelectBtn.isChecked = count == list.size
}
checkList?.set(which, isChecked)
}
補足
setMarginなどで使われているUIHelper.convertDp2PxAsInt(20, context)
の内容は以下。
引数として与えられたdpをpxに変換している。
fun convertDp2PxAsInt(dp: Int, context: Context): Int {
return (dp * context.resources.displayMetrics.density).toInt()
}
Discussion