feat(modal): V2 공통 모달창을 추가한다
This commit is contained in:
@@ -0,0 +1,77 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.components.modal
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.WindowManager
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.core.graphics.drawable.toDrawable
|
||||||
|
import kr.co.vividnext.sodalive.databinding.DialogV2ModalBinding
|
||||||
|
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||||
|
|
||||||
|
class V2ModalDialog(
|
||||||
|
activity: Activity,
|
||||||
|
layoutInflater: LayoutInflater,
|
||||||
|
title: String,
|
||||||
|
desc: String,
|
||||||
|
confirmButtonTitle: String,
|
||||||
|
confirmButtonClick: () -> Unit,
|
||||||
|
cancelButtonTitle: String = "",
|
||||||
|
cancelButtonClick: (() -> Unit)? = null
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val alertDialog: AlertDialog
|
||||||
|
private var isButtonClicked = false
|
||||||
|
|
||||||
|
val dialogView = DialogV2ModalBinding.inflate(layoutInflater)
|
||||||
|
|
||||||
|
init {
|
||||||
|
val dialogBuilder = AlertDialog.Builder(activity)
|
||||||
|
dialogBuilder.setView(dialogView.root)
|
||||||
|
|
||||||
|
alertDialog = dialogBuilder.create()
|
||||||
|
alertDialog.setCancelable(false)
|
||||||
|
alertDialog.window?.setBackgroundDrawable(Color.TRANSPARENT.toDrawable())
|
||||||
|
|
||||||
|
dialogView.tvTitle.text = title
|
||||||
|
dialogView.tvDesc.text = desc
|
||||||
|
|
||||||
|
dialogView.tvCancel.text = cancelButtonTitle
|
||||||
|
dialogView.tvCancel.setOnClickListener {
|
||||||
|
dismissAndRun {
|
||||||
|
cancelButtonClick?.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dialogView.tvConfirm.text = confirmButtonTitle
|
||||||
|
dialogView.tvConfirm.setOnClickListener {
|
||||||
|
dismissAndRun(confirmButtonClick)
|
||||||
|
}
|
||||||
|
|
||||||
|
dialogView.tvCancel.visibility = if (cancelButtonTitle.isNotBlank()) {
|
||||||
|
View.VISIBLE
|
||||||
|
} else {
|
||||||
|
View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun show(width: Int) {
|
||||||
|
alertDialog.show()
|
||||||
|
|
||||||
|
val lp = WindowManager.LayoutParams()
|
||||||
|
lp.copyFrom(alertDialog.window?.attributes)
|
||||||
|
lp.width = width - (26.7f.dpToPx()).toInt()
|
||||||
|
lp.height = WindowManager.LayoutParams.WRAP_CONTENT
|
||||||
|
|
||||||
|
alertDialog.window?.setBackgroundDrawable(Color.TRANSPARENT.toDrawable())
|
||||||
|
alertDialog.window?.attributes = lp
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun dismissAndRun(action: () -> Unit) {
|
||||||
|
if (isButtonClicked) return
|
||||||
|
isButtonClicked = true
|
||||||
|
alertDialog.dismiss()
|
||||||
|
action()
|
||||||
|
}
|
||||||
|
}
|
||||||
5
app/src/main/res/drawable/bg_v2_modal.xml
Normal file
5
app/src/main/res/drawable/bg_v2_modal.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<solid android:color="@color/gray_900" />
|
||||||
|
<corners android:radius="@dimen/radius_14" />
|
||||||
|
</shape>
|
||||||
71
app/src/main/res/layout/dialog_v2_modal.xml
Normal file
71
app/src/main/res/layout/dialog_v2_modal.xml
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:background="@drawable/bg_v2_modal"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fontFamily="@font/bold"
|
||||||
|
android:gravity="center"
|
||||||
|
android:minHeight="64dp"
|
||||||
|
android:paddingHorizontal="@dimen/spacing_20"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:textSize="20sp"
|
||||||
|
tools:text="모달" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_desc"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fontFamily="@font/regular"
|
||||||
|
android:gravity="center"
|
||||||
|
android:lineSpacingMultiplier="1.45"
|
||||||
|
android:padding="@dimen/spacing_20"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:textSize="16sp"
|
||||||
|
tools:text="모달 안내 문구가\n들어가는 부분입니다." />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/layout_button"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingHorizontal="@dimen/spacing_20"
|
||||||
|
android:paddingVertical="@dimen/spacing_14"
|
||||||
|
tools:ignore="UseCompoundDrawables">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_cancel"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="@dimen/spacing_8"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:fontFamily="@font/medium"
|
||||||
|
android:gravity="center"
|
||||||
|
android:padding="@dimen/spacing_14"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:textSize="18sp"
|
||||||
|
tools:text="취소" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_confirm"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:fontFamily="@font/medium"
|
||||||
|
android:gravity="center"
|
||||||
|
android:padding="@dimen/spacing_14"
|
||||||
|
android:textColor="@color/soda_400"
|
||||||
|
android:textSize="18sp"
|
||||||
|
tools:text="확인" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.components.modal
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.view.View
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.robolectric.Robolectric
|
||||||
|
import org.robolectric.RobolectricTestRunner
|
||||||
|
import org.robolectric.annotation.Config
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
@RunWith(RobolectricTestRunner::class)
|
||||||
|
@Config(sdk = [28], application = Application::class)
|
||||||
|
class V2ModalDialogTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `cancelButtonTitle이 blank이면 확인 버튼만 표시한다`() {
|
||||||
|
val activity = Robolectric.buildActivity(FragmentActivity::class.java).setup().get()
|
||||||
|
var confirmClickCount = 0
|
||||||
|
val dialog = V2ModalDialog(
|
||||||
|
activity = activity,
|
||||||
|
layoutInflater = activity.layoutInflater,
|
||||||
|
title = "안내",
|
||||||
|
desc = "확인만 있는 모달",
|
||||||
|
confirmButtonTitle = "확인",
|
||||||
|
confirmButtonClick = { confirmClickCount += 1 },
|
||||||
|
cancelButtonTitle = " "
|
||||||
|
)
|
||||||
|
|
||||||
|
dialog.show(width = 360)
|
||||||
|
val alertDialog = alertDialogOf(dialog)
|
||||||
|
assertTrue(alertDialog.isShowing)
|
||||||
|
dialog.dialogView.tvConfirm.performClick()
|
||||||
|
|
||||||
|
assertEquals(View.GONE, dialog.dialogView.tvCancel.visibility)
|
||||||
|
assertEquals(View.VISIBLE, dialog.dialogView.tvConfirm.visibility)
|
||||||
|
assertEquals("확인", dialog.dialogView.tvConfirm.text.toString())
|
||||||
|
assertFalse(alertDialog.isShowing)
|
||||||
|
assertEquals(1, confirmClickCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `확인 버튼을 빠르게 여러 번 눌러도 액션은 한 번만 실행한다`() {
|
||||||
|
val activity = Robolectric.buildActivity(FragmentActivity::class.java).setup().get()
|
||||||
|
var confirmClickCount = 0
|
||||||
|
val dialog = V2ModalDialog(
|
||||||
|
activity = activity,
|
||||||
|
layoutInflater = activity.layoutInflater,
|
||||||
|
title = "안내",
|
||||||
|
desc = "중복 클릭 방지",
|
||||||
|
confirmButtonTitle = "확인",
|
||||||
|
confirmButtonClick = { confirmClickCount += 1 }
|
||||||
|
)
|
||||||
|
|
||||||
|
dialog.show(width = 360)
|
||||||
|
dialog.dialogView.tvConfirm.performClick()
|
||||||
|
dialog.dialogView.tvConfirm.performClick()
|
||||||
|
|
||||||
|
assertEquals(1, confirmClickCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `cancelButtonTitle이 있으면 두 버튼을 표시하고 취소 액션을 실행한다`() {
|
||||||
|
val activity = Robolectric.buildActivity(FragmentActivity::class.java).setup().get()
|
||||||
|
var cancelClickCount = 0
|
||||||
|
val dialog = V2ModalDialog(
|
||||||
|
activity = activity,
|
||||||
|
layoutInflater = activity.layoutInflater,
|
||||||
|
title = "삭제",
|
||||||
|
desc = "정말 삭제할까요?",
|
||||||
|
confirmButtonTitle = "삭제",
|
||||||
|
confirmButtonClick = {},
|
||||||
|
cancelButtonTitle = "취소",
|
||||||
|
cancelButtonClick = { cancelClickCount += 1 }
|
||||||
|
)
|
||||||
|
|
||||||
|
dialog.show(width = 360)
|
||||||
|
val alertDialog = alertDialogOf(dialog)
|
||||||
|
assertTrue(alertDialog.isShowing)
|
||||||
|
dialog.dialogView.tvCancel.performClick()
|
||||||
|
|
||||||
|
assertEquals(View.VISIBLE, dialog.dialogView.tvCancel.visibility)
|
||||||
|
assertEquals(View.VISIBLE, dialog.dialogView.tvConfirm.visibility)
|
||||||
|
assertEquals("취소", dialog.dialogView.tvCancel.text.toString())
|
||||||
|
assertEquals("삭제", dialog.dialogView.tvConfirm.text.toString())
|
||||||
|
assertFalse(alertDialog.isShowing)
|
||||||
|
assertEquals(1, cancelClickCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `title과 desc는 파라미터 값을 표시한다`() {
|
||||||
|
val activity = Robolectric.buildActivity(FragmentActivity::class.java).setup().get()
|
||||||
|
val dialog = V2ModalDialog(
|
||||||
|
activity = activity,
|
||||||
|
layoutInflater = activity.layoutInflater,
|
||||||
|
title = "모달 제목",
|
||||||
|
desc = "첫 줄\n두 번째 줄",
|
||||||
|
confirmButtonTitle = "확인",
|
||||||
|
confirmButtonClick = {}
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals("모달 제목", dialog.dialogView.tvTitle.text.toString())
|
||||||
|
assertEquals("첫 줄\n두 번째 줄", dialog.dialogView.tvDesc.text.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `cancelButtonClick이 null이어도 취소 버튼은 모달을 닫는다`() {
|
||||||
|
val activity = Robolectric.buildActivity(FragmentActivity::class.java).setup().get()
|
||||||
|
val dialog = V2ModalDialog(
|
||||||
|
activity = activity,
|
||||||
|
layoutInflater = activity.layoutInflater,
|
||||||
|
title = "안내",
|
||||||
|
desc = "취소 액션 없음",
|
||||||
|
confirmButtonTitle = "확인",
|
||||||
|
confirmButtonClick = {},
|
||||||
|
cancelButtonTitle = "취소"
|
||||||
|
)
|
||||||
|
|
||||||
|
dialog.show(width = 360)
|
||||||
|
val alertDialog = alertDialogOf(dialog)
|
||||||
|
assertTrue(alertDialog.isShowing)
|
||||||
|
dialog.dialogView.tvCancel.performClick()
|
||||||
|
|
||||||
|
assertFalse(alertDialog.isShowing)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `layout과 drawable은 Figma 확정 리소스 계약을 포함한다`() {
|
||||||
|
val layout = projectFile("app/src/main/res/layout/dialog_v2_modal.xml").readText()
|
||||||
|
val background = projectFile("app/src/main/res/drawable/bg_v2_modal.xml").readText()
|
||||||
|
|
||||||
|
assertTrue(layout.contains("android:background=\"@drawable/bg_v2_modal\""))
|
||||||
|
assertTrue(layout.contains("android:id=\"@+id/tv_title\""))
|
||||||
|
assertTrue(layout.contains("android:minHeight=\"64dp\""))
|
||||||
|
assertTrue(layout.contains("android:id=\"@+id/tv_desc\""))
|
||||||
|
assertTrue(layout.contains("android:id=\"@+id/tv_cancel\""))
|
||||||
|
assertTrue(layout.contains("android:id=\"@+id/tv_confirm\""))
|
||||||
|
assertTrue(layout.contains("android:textColor=\"@color/soda_400\""))
|
||||||
|
assertTrue(layout.contains("android:textColor=\"@color/white\""))
|
||||||
|
assertTrue(background.contains("@color/gray_900"))
|
||||||
|
assertTrue(background.contains("@dimen/radius_14"))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun projectFile(path: String): File = File(projectRoot(), path)
|
||||||
|
|
||||||
|
private fun alertDialogOf(dialog: V2ModalDialog): AlertDialog {
|
||||||
|
val field = V2ModalDialog::class.java.getDeclaredField("alertDialog")
|
||||||
|
field.isAccessible = true
|
||||||
|
return field.get(dialog) as AlertDialog
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun projectRoot(): File {
|
||||||
|
return generateSequence(File(System.getProperty("user.dir") ?: ".").absoluteFile) { it.parentFile }
|
||||||
|
.first { File(it, "settings.gradle").exists() }
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user