説明
ActivityとFragmentから使用でき、ボタンは縦並びで何個でも追加できる設計とします。
このような見た目になります。
1.レイアウト
1-1.DialogFragmentにinflateするレイアウトファイル
<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ファイル
<?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ファイル
<?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することで、同じダイアログが複数個表示されないよう制御しています。
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()
を実行するとダイアログが閉じます。
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を渡すのを忘れないようにしてください。
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()
}
}
以上