diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/components/modal/V2ModalDialog.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/components/modal/V2ModalDialog.kt new file mode 100644 index 00000000..76036ba3 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/components/modal/V2ModalDialog.kt @@ -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() + } +} diff --git a/app/src/main/res/drawable/bg_v2_modal.xml b/app/src/main/res/drawable/bg_v2_modal.xml new file mode 100644 index 00000000..c410cc4e --- /dev/null +++ b/app/src/main/res/drawable/bg_v2_modal.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/layout/dialog_v2_modal.xml b/app/src/main/res/layout/dialog_v2_modal.xml new file mode 100644 index 00000000..907d762c --- /dev/null +++ b/app/src/main/res/layout/dialog_v2_modal.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/test/java/kr/co/vividnext/sodalive/v2/components/modal/V2ModalDialogTest.kt b/app/src/test/java/kr/co/vividnext/sodalive/v2/components/modal/V2ModalDialogTest.kt new file mode 100644 index 00000000..1aca224d --- /dev/null +++ b/app/src/test/java/kr/co/vividnext/sodalive/v2/components/modal/V2ModalDialogTest.kt @@ -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() } + } +}