LoginSignup
2
4

More than 3 years have passed since last update.

【Android】ActivityとFragmentへコールバックできる汎用ダイアログを作る

Last updated at Posted at 2020-05-13

説明

ActivityとFragmentから使用でき、ボタンは縦並びで何個でも追加できる設計とします。
このような見た目になります。
Screenshot_1589356952.png

1.レイアウト

1-1.DialogFragmentにinflateするレイアウトファイル

fragment_vertical_button_dialog.xml
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/rootLayout"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@drawable/dialog_border"
    android:paddingTop="10dp">

    <TextView
        android:id="@+id/title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="center_horizontal"
        android:paddingLeft="15dp"
        android:paddingRight="15dp"
        android:textSize="18sp"
        android:textStyle="bold"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/message"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:gravity="center_horizontal"
        android:paddingLeft="15dp"
        android:paddingRight="15dp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@id/title" />

    <View
        android:id="@+id/firstBorder"
        android:layout_width="0dp"
        android:layout_height="@dimen/border_height"
        android:layout_marginTop="10dp"
        android:background="@android:color/darker_gray"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@id/message" />

</androidx.constraintlayout.widget.ConstraintLayout>

1-2.drawableファイル

1-2-1.ダイアログを角丸にするためのdrawableファイル

dialog_border.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item>
        <shape android:shape="rectangle">
            <corners
                android:topRightRadius="10dp"
                android:bottomRightRadius="10dp"
                android:bottomLeftRadius="10dp"
                android:topLeftRadius="10dp"/>
            <solid android:color="@android:color/white" />
        </shape>
    </item>
</selector>

1-2-2.最後のボタンの下側を角丸にするためのdrawableファイル

vertical_dialog_last_button.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item>
        <shape android:shape="rectangle">
            <corners
                android:bottomRightRadius="10dp"
                android:bottomLeftRadius="10dp"/>
            <solid android:color="@android:color/white" />
        </shape>
    </item>
</selector>

2.DialogFragment

インスタンス化する際のnewInstanceメソッドで可変長引数となっているbuttonInfoListが任意の個数のButtonInfoのインスタンスを受け取り、その分の罫線とボタンがaddBorderメソッドとaddButtonメソッドによって追加される仕組みになっています。

onAttachメソッドをoverrideしnewInstanceメソッドで受け取ったisActivityの値を用いてActivity、Fragmentどちらから使用されてもコールバックできるようにしています。呼び出し元でVerticalButtonDialogClickListenerが実装されていない場合はここで実行時例外をthrowするようにしています。(throwする例外がRuntimeExceptionなのは変だ。という指摘は大丈夫です。)

また、showメソッドとshowNowメソッドをoverrideすることで、同じダイアログが複数個表示されないよう制御しています。

VerticalButtonDialogFragment.kt
package jp.takumi.commondialogexample

import android.content.Context
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.content.ContextCompat
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentManager
import kotlinx.android.synthetic.main.fragment_vertical_button_dialog.view.*
import java.io.Serializable
import kotlin.math.roundToInt


/**
 * ボタン縦並び角丸ダイアログ
 * タイトルと説明TextViewつき
 */
class VerticalButtonDialogFragment : DialogFragment() {
    interface VerticalButtonDialogClickListener {
        fun result(resultCode: Int, dismiss: () -> Unit)
    }
    data class ButtonInfo(val button: Int, val resultCode: Int): Serializable

    companion object {
        private const val ARG_TITLE = "ARG_TITLE"
        private const val ARG_MESSAGE = "ARG_MESSAGE"
        private const val ARG_IS_ACTIVITY = "ARG_IS_ACTIVITY"
        private const val ARG_BUTTON_INFOS = "ARG_BUTTON_INFOS"
        @JvmStatic
        fun newInstance(titleId: Int, messageId: Int, isActivity: Boolean, vararg buttonInfoList: ButtonInfo) =
                VerticalButtonDialogFragment().apply {
                    arguments = Bundle().apply {
                        putInt(ARG_TITLE, titleId)
                        putInt(ARG_MESSAGE, messageId)
                        putBoolean(ARG_IS_ACTIVITY, isActivity)
                        putSerializable(ARG_BUTTON_INFOS, buttonInfoList)
                    }
                }
    }

    private lateinit var clickListener: VerticalButtonDialogClickListener

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        val aArguments = arguments ?: throw RuntimeException("argumentsがnull")
        dialog?.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
        isCancelable = false

        val view = inflater.inflate(R.layout.fragment_vertical_button_dialog, container, false)
        view.title.text = getString(aArguments.getInt(ARG_TITLE))
        view.message.text = getString(aArguments.getInt(ARG_MESSAGE))

        var parentView: View = view.rootLayout.firstBorder
        val buttonInfoList = aArguments.getSerializable(ARG_BUTTON_INFOS) as Array<ButtonInfo>
        buttonInfoList.forEachIndexed { index, buttonInfo ->
            parentView = addButton(view.rootLayout, parentView.id, index + 1, buttonInfo)
            if (index + 1 != buttonInfoList.size) parentView = addBorder(view.rootLayout, parentView.id, index + 1)
        }

        return view
    }

    override fun onAttach(context: Context) {
        super.onAttach(context)

        val aArguments = arguments ?: throw RuntimeException("argumentsがnull")

        try {
            clickListener = if (aArguments.getBoolean(ARG_IS_ACTIVITY)) {
                context as VerticalButtonDialogClickListener
            } else {
                targetFragment as VerticalButtonDialogClickListener
            }
        } catch (e: ClassCastException) {
            throw RuntimeException("呼び出し元でVerticalButtonDialogClickListenerが実装されていない")
        }
    }

    override fun show(manager: FragmentManager, tag: String?) {
        if (!isAlreadyDisplayed(manager, tag)) super.show(manager, tag)
    }

    override fun showNow(manager: FragmentManager, tag: String?) {
        if (!isAlreadyDisplayed(manager, tag)) super.showNow(manager, tag)
    }

    /**
     * ボタンの間のborderを追加
     */
    private fun addBorder(rootLayout: ConstraintLayout, parentId: Int, index: Int): View {
        val aContext = context ?: throw RuntimeException("contextがnull")

        val border = View(aContext).apply {
            id = index
            setBackgroundColor(ContextCompat.getColor(aContext, android.R.color.darker_gray))
        }
        rootLayout.addView(border)

        // 制約作り
        ConstraintSet().apply {
            clone(rootLayout)
            // width heightの設定
            constrainHeight(border.id, aContext.resources.getDimension(R.dimen.border_height).roundToInt())
            constrainWidth(border.id, ConstraintSet.MATCH_CONSTRAINT)
            // Top_toBottomOf
            connect(border.id, ConstraintSet.TOP, parentId, ConstraintSet.BOTTOM)
            // Left_toLeftOf
            connect(border.id, ConstraintSet.LEFT, ConstraintSet.PARENT_ID, ConstraintSet.LEFT)
            // Right_toRightOf
            connect(border.id, ConstraintSet.RIGHT, ConstraintSet.PARENT_ID, ConstraintSet.RIGHT)
            // 反映
            applyTo(rootLayout)
        }

        return border
    }

    /**
     * buttonを追加
     */
    private fun addButton(rootLayout: ConstraintLayout, parentId: Int, index: Int, buttonInfo: ButtonInfo): View {
        val aArguments = arguments ?: throw RuntimeException("argumentsがnull")
        val aContext = context ?: throw RuntimeException("contextがnull")

        val button = Button(aContext).apply {
            id = index * 100 // 100個以上のボタンを表示することはないのでバッティングすることはないだろう
            text = getString(buttonInfo.button)
            setOnClickListener {
                clickListener.result(buttonInfo.resultCode) { this@VerticalButtonDialogFragment.dismiss() }
            }
            setTextColor(ContextCompat.getColor(aContext, android.R.color.holo_blue_dark))
            setTextSize(TypedValue.COMPLEX_UNIT_PX, resources.getDimension(R.dimen.button_text_size))
            setBackgroundColor(ContextCompat.getColor(aContext, android.R.color.white))
            if (index == (aArguments.getSerializable(ARG_BUTTON_INFOS) as Array<ButtonInfo>).size) setBackgroundResource(R.drawable.vertical_dialog_last_button)
        }
        rootLayout.addView(button)

        // 制約作り
        ConstraintSet().apply {
            clone(rootLayout)
            // width heightの設定
            constrainHeight(button.id, ConstraintSet.WRAP_CONTENT)
            constrainWidth(button.id, ConstraintSet.MATCH_CONSTRAINT)
            // Top_toBottomOf
            connect(button.id, ConstraintSet.TOP, parentId, ConstraintSet.BOTTOM)
            // Left_toLeftOf
            connect(button.id, ConstraintSet.LEFT, ConstraintSet.PARENT_ID, ConstraintSet.LEFT)
            // Right_toRightOf
            connect(button.id, ConstraintSet.RIGHT, ConstraintSet.PARENT_ID, ConstraintSet.RIGHT)
            // 反映
            applyTo(rootLayout)
        }

        return button
    }

    /**
     * 同一タグのダイアログが表示済みが判定する
     * true: 表示されている false: 表示されていない
     */
    private fun isAlreadyDisplayed(manager: FragmentManager?, tag: String?): Boolean {
        val prevDialog = manager?.findFragmentByTag(tag) as? DialogFragment
        return prevDialog?.dialog?.isShowing ?: false
    }
}

3.使う

3-1.Activityから使う

ただnewInstanceメソッドでインスタンス化し、showメソッドを呼ぶだけです。普通のDialogFragmentと一緒ですね。
引数のisActivityにはActivityが使用するのでtrueを渡しましょう。
コールバックメソッドの引数のdismiss()を実行するとダイアログが閉じます。

MainActivity.kt
package jp.takumi.commondialogexample

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity(), VerticalButtonDialogFragment.VerticalButtonDialogClickListener {

    companion object {
        private const val BUTTON_1 = 1
        private const val BUTTON_2 = 2
        private const val BUTTON_3 = 3
        private const val BUTTON_4 = 4
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        showDialog.setOnClickListener {
            VerticalButtonDialogFragment.newInstance(
                R.string.title,
                R.string.message,
                true,
                VerticalButtonDialogFragment.ButtonInfo(R.string.button1, BUTTON_1),
                VerticalButtonDialogFragment.ButtonInfo(R.string.button2, BUTTON_2),
                VerticalButtonDialogFragment.ButtonInfo(R.string.button3, BUTTON_3),
                VerticalButtonDialogFragment.ButtonInfo(R.string.button4, BUTTON_4)
            ).show(supportFragmentManager, "example")
        }
    }

    override fun result(resultCode: Int, dismiss: () -> Unit) {
        dismiss()
        when(resultCode) {
            BUTTON_1 -> {
                Toast.makeText(this, "BUTTON1", Toast.LENGTH_SHORT).show()
            }
            BUTTON_2 -> {
                Toast.makeText(this, "BUTTON2", Toast.LENGTH_SHORT).show()
            }
            BUTTON_3 -> {
                Toast.makeText(this, "BUTTON3", Toast.LENGTH_SHORT).show()
            }
            BUTTON_4 -> {
                Toast.makeText(this, "BUTTON4", Toast.LENGTH_SHORT).show()
            }
        }
    }
}

3-2.Fragmentから使う

Activityと異なるのはsetTargetFragmentメソッドを呼ぶことです。このメソッドを呼ぶこととisActivityにfalseを渡すのを忘れないようにしてください。

MainFragment.kt
package jp.takumi.commondialogexample

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.Fragment
import kotlinx.android.synthetic.main.fragment_main.*
import java.lang.RuntimeException

class MainFragment : Fragment(), VerticalButtonDialogFragment.VerticalButtonDialogClickListener {

    companion object {
        private const val BUTTON_1 = 1
        @JvmStatic
        fun newInstance() = MainFragment()
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        return inflater.inflate(R.layout.fragment_main, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        showDialog.setOnClickListener {
            val aFragmentManager = fragmentManager ?: throw RuntimeException("fragmentManager is null")
            val dialog = VerticalButtonDialogFragment.newInstance(
                R.string.title,
                R.string.message,
                false,
                VerticalButtonDialogFragment.ButtonInfo(R.string.button1, BUTTON_1)
            )
            dialog.setTargetFragment(this, 0)
            dialog.show(aFragmentManager, "fragment_example")
        }
    }

    override fun result(resultCode: Int, dismiss: () -> Unit) {
        dismiss()
        Toast.makeText(activity, "BUTTON1", Toast.LENGTH_SHORT).show()
    }
}

以上

2
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
4