본문 바로가기
오늘은 뭘 배울까?/Android

안드로이드 다이얼로그 만들기(Custom Dialog까지)

by Kim Juhwan 2022. 9. 9.

1. Dialog
   1-1. 개념
   1-2. 생명주기
2. 기본 Dialog
   2-1. 텍스트만
   2-2. 부정/긍정 버튼

   2-3. 부정/긍정/중립 버튼
   2-4. 리스트
   2-5. 라디오 버튼
   2-6. 체크박스
3. Custom Dialog
   3-1. Dialog를 상속받는 방법
   3-2. theme를 만드는 방법

4. 예제 링크

 

 

 

 


 

 

1. Dialog

1-1. 개념

위메프 앱 메인에서 사용되고 있는 Dialog

 

다이얼로그는 사용자에게 결정을 내리거나 추가 정보를 입력하라는 메시지를 표시하는 작은 창이다.

보통 사용자가 다음으로 진행하기 전에 조치를 취해야 하는 모달 이벤트에 사용된다.

실제 사용 예시를 들자면 위 스크린샷처럼 앱 접속 시 뜨는 광고에도 쓰이고

뭔가를 삭제할 때 정말 삭제하시겠습니까? 처럼 의견을 물을 때도 쓰이고 사용처가 다양하다.

 

1-2. 생명주기

액티비티 전환 시 생명주기

 

A 액티비티에서 B 액티비티로 갔다가 오면

onStop - onRestart - onStart의 생명주기를 가진다.

 

다이얼로그 전환 시 생명주기

 

반면 다이얼로그를 띄웠다가 닫을 시

onPause - onResume 순서로 호출이 된다.

 

즉, 다른 액티비티를 띄우면 원래 액티비티가 정지 되었다가(onStop) 다시 실행되는 순서지만

다이얼로그는 원래 액티비티가 정지되는 것이 아니라 잠시 포커스를 잃고(onPause) 다시 실행되는 것이다.

 

2. 기본 Dialog

2-1. 텍스트만

텍스트만 띄운 다이얼로그

 

    private fun materialBasicDialog() {
        MaterialAlertDialogBuilder(this)
            .setMessage("Hello, I am a basic dialog")
            .show()
    }

이렇게 사용할 일은 많이 없겠지만...

텍스트만 띄우는 경우이다.

MaterialAlertDialogBuilder를 통해 작성할 메시지를 설정하고 show 해주면 끝이다. 아주 간단

 

2-2. 부정/긍정 버튼

부정/긍정 버튼이 있는 다이얼로그

 

    private fun materialNegativePositiveDialog() {
        MaterialAlertDialogBuilder(this)
            .setMessage("Hello, I have negative & positive button.")
            .setNegativeButton("cancel") { dialog, which ->
                // Respond to negative button press
            }
            .setPositiveButton("confirm") { dialog, which ->
                // Respond to positive button press
            }
            .show()
    }

가장 기본적인 다이얼로그의 형태이다.

버튼에 들어가는 단어는 변경할 수 있지만 Negative와 Positive의 위치를 바꿀 수는 없다.

구글에서 권장하고 제공하는 버튼의 순서는 부정이 왼쪽, 긍정이 오른쪽이기 때문이다.

 

추가로, Yes나 No보다는 Cancel과 Save, Cancel과 Remove처럼 명확하고 직관적인 단어를 사용하는 것이 UX 측면에서 좋다고 한다.

 

2-3. 부정/긍정/중립 버튼

중립버튼이 추가된 다이얼로그

 

    private fun materialNeutralDialog() {
        MaterialAlertDialogBuilder(this)
            .setMessage("Hello, I have one more neutral button added")
            .setNeutralButton("cancel") { dialog, which ->
                // Respond to neutral button press
            }
            .setNegativeButton("decline") { dialog, which ->
                // Respond to negative button press
            }
            .setPositiveButton("accept") { dialog, which ->
                // Respond to positive button press
            }
            .show()
    }

선택지가 하나 더 늘어났다고 보면 된다.

거절과 승인 그리고 취소

[목차 2-2]에서 Cancel을 negative button으로 사용해서 조금 헷갈릴 수 있는데

이번 예제에서는 중립 버튼이 Cancel 역할을 한다는 것을 유의하자

 

2-4. 리스트

다이얼로그 + 리스트

 

    private fun materialListDialog() {
        val items = arrayOf("banana", "apple", "watermelon")

        MaterialAlertDialogBuilder(this)
            .setTitle("Choose what you like")
            //.setMessage("DO NOT SET MESSAGE WHEN YOU USE LIST")
            .setItems(items) { dialog, which ->
                showToastMessage("Oh, you like ${items[which]}!")
            }
            .show()
    }

다이얼로그에 리스트가 있는 형태이다.

아이템을 클릭하면 setItems이 호출되고 두 번째 매개변수(which)를 통해 아이템의 인덱스를 알 수 있다.

주의할 점은 setMessage는 리스트와 같이 사용될 수 없다는 것이다.

만약 사용하면 리스트보다 우선순위를 가지기 때문에 메시지만 뜨게 된다.

 

2-5. 라디오 버튼

다이얼로그 + 라디오버튼

 

    private fun materialRadioDialog() {
        val singleItems = arrayOf("banana", "apple", "watermelon")
        var checkedItem = 0

        MaterialAlertDialogBuilder(this)
            .setTitle("Choose what you like")
            .setNeutralButton("cancel") { dialog, which ->
                // Respond to neutral button press
            }
            .setPositiveButton("ok") { dialog, which ->
                showToastMessage("Oh, you like ${singleItems[checkedItem]}!")
            }
            // Single-choice items (initialized with checked item)
            .setSingleChoiceItems(singleItems, checkedItem) { dialog, which ->
                checkedItem = which
            }
            .show()
    }

setSingleChoiceItems를 사용하여 다이얼로그에 라디오 버튼을 쉽게 적용할 수 있다.

처음에 몇 번째 아이템이 선택될지 기본 값을 설정해줘야 하는데 위 코드에서는 checkedItem이라는 이름으로 0을 주었다.

그리고 라디오버튼 클릭 시 클릭된 인덱스의 값이 checkedItem으로 들어가는 형태이다.

 

2-6. 체크박스

다이얼로그 + 체크박스

 

    private fun materialCheckBoxDialog() {
        val multiItems = arrayOf("Item 1", "Item 2", "Item 3")
        val checkedItems = booleanArrayOf(true, false, false, false)

        MaterialAlertDialogBuilder(this)
            .setTitle("Choose what you like")
            .setNeutralButton("cancel") { dialog, which ->
                // Respond to neutral button press
            }
            .setPositiveButton("ok") { dialog, which ->
                val checkCnt = checkedItems.count { it }
                showToastMessage("you choose $checkCnt items")
            }
            // Single-choice items (initialized with checked item)
            .setMultiChoiceItems(multiItems, checkedItems) { dialog, which, checked ->
                checkedItems[which] = checked
            }
            .show()
    }

라디오 버튼은 한 개의 선택지만 기억하면 되므로 Int형 변수를 사용했지만

체크박스는 여러 개의 선택지를 기억해야 하므로 boolean형 배열을 사용한다.

딱히 어려울 것 없어서 설명은 패쓰!

 

3. Custom Dialog

3-1. Dialog를 상속받는 방법

[목차 2]에서 설명한 다이얼로그는 Material Dialog라고 해서

구글이 Dialog를 좀 쉽게 사용할 수 있게 만든 거고

이게 우리가 제안하는 가장 기본적인 표준 디자인(UX)이야! 라고 하면서 만든 다이얼로그이다.

 

이번 목차에서는 우리가 다이얼로그를 커스텀하는 2가지 방법에 대해서 알아보려고 한다.

먼저, 직접 Dialog를 상속받아 커스텀하는 방법을 알아보자.

 

커스텀 다이얼로그 예제 1

 

    private fun customDialog() {
        val dialog = CustomDialog(this)

        dialog.setItemClickListener(object : CustomDialog.ItemClickListener{
            override fun onClick(tel: String) {
                showToastMessage("${tel}로 전화를 걸겠습니다.")
            }
        })

        dialog.show()
    }

CustomDialog라는 클래스 파일을 따로 만들어서 거기서 다이얼로그를 생성해 줄 예정이다.

액티비티에서는 객체를 만들고 리스너 달고 show 해주는 게 전부이다.

 

class CustomDialog(context: Context): Dialog(context) {
    private lateinit var itemClickListener: ItemClickListener
    private lateinit var binding: DialogCustomBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DialogCustomBinding.inflate(LayoutInflater.from(context))
        setContentView(binding.root)

        // 사이즈를 조절하고 싶을 때 사용 (use it when you want to resize dialog)
        // resize(this, 0.8f, 0.4f)

        // 배경을 투명하게 (Make the background transparent)
        // 다이얼로그를 둥글게 표현하기 위해 필요 (Required to round corner)
        window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))

        // 다이얼로그 바깥쪽 클릭시 종료되도록 함 (Cancel the dialog when you touch outside)
        setCanceledOnTouchOutside(true)

        // 취소 가능 유무
        setCancelable(true)

        binding.tvCancel.setOnClickListener {
            dismiss() // 다이얼로그 닫기 (Close the dialog)
        }

        binding.tvCall.setOnClickListener {
            // interface를 이용해 다이얼로그를 호출한 곳에 값을 전달함
            // Use interface to pass the value to the activty or fragment
            itemClickListener.onClick("031-467-0000")
            dismiss()
        }
    }

    // 사이즈를 조절하고 싶을 때 사용 (use it when you want to resize dialog)
    private fun resize(dialog: Dialog, width: Float, height: Float){
        val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager

        if (Build.VERSION.SDK_INT < 30) {
            val size = Point()
            windowManager.defaultDisplay.getSize(size)

            val x = (size.x * width).toInt()
            val y = (size.y * height).toInt()
            dialog.window?.setLayout(x, y)
        } else {
            val rect = windowManager.currentWindowMetrics.bounds

            val x = (rect.width() * width).toInt()
            val y = (rect.height() * height).toInt()
            dialog.window?.setLayout(x, y)
        }
    }

    interface ItemClickListener {
        fun onClick(message: String)
    }

    fun setItemClickListener(itemClickListener: ItemClickListener) {
        this.itemClickListener = itemClickListener
    }
}

주석을 달아놔서 좀 길게 느껴질 수 있는데 사실 되게 간단하다.

그리고 사이즈 조절도 필요 없으면 resize() 메서드는 그냥 지워버려도 된다.

다이얼로그의 클릭 이벤트를 외부에서 전달받는 방법에는 여러 가지가 있고 본인이 편한 방법을 사용하면 된다.

나는 interface를 내부에 만들어서 사용하는 방법을 선택했다.

 

설명은 주석으로 다 달아두었고 각 명령어들을 한 줄씩 지워보면서

무엇이 달라지나 눈으로 확인해보면 더 잘 와닿을 것이다.

 

3-2. theme를 만드는 방법

커스텀 다이얼로그 예제 2

 

    private fun customDialog2() {
        val binding = DialogCustom2Binding.inflate(LayoutInflater.from(this))
        val dialog = MaterialAlertDialogBuilder(this, R.style.CustomDialog2Theme)
            .setView(binding.root)
            .create()

        binding.btnConfirm.setOnClickListener {
            showToastMessage("확인을 눌렀습니다.")
        }

        dialog.show()
    }

이번에는 Dialog를 상속받지 않고 [목차 2]에서 하던 대로 MaterialAlertDialogBuilder를 사용한다.

대신 이번에는 2번째 인자로 theme를 넘겨주고

setView로 우리가 만든 xml을 적용한다.

이렇게 하면 리스너도 [목차 3-1]처럼 복잡하게 설정하지 않아도 된다.

 

.
.
.
    <style name="CustomDialog2Theme">
        <item name="android:backgroundTint">@android:color/transparent</item>
    </style>
.
.
.

values - themes - themes.xml에 위 코드를 작성해주면 된다.

Dialog에 적용할 속성을 적어주면 되는데 예를 들어 위 코드는 배경을 투명하게 해주는 속성이다.

 

    <style name="CustomDialog2Theme">
        <item name="android:backgroundTint">@android:color/transparent</item>
        <item name="android:windowMinWidthMajor">50%</item>
        <item name="android:windowMinWidthMinor">50%</item>
    </style>

만약 원하는 속성이 있으면 이렇게 추가해주면 된다.

(새롭게 추가된 코드는 가로 세로 크기를 지정하는 속성이다)

 

4. 예제 링크

 

GitHub - juhwankim-dev/SelfStudy: 코틀린으로 공부한 것들을 올리는 공간입니다.

코틀린으로 공부한 것들을 올리는 공간입니다. Contribute to juhwankim-dev/SelfStudy development by creating an account on GitHub.

github.com

모든 코드를 포스팅에 다 넣지는 못했습니다. (너무 길어지므로...)

만약 drawable이나 xml의 코드가 필요하시다면

깃허브 레포지토리에 올려두었으므로 참고해주시면 됩니다 😊

 

질문이 있으시면 얼마든지 남겨주세요!

 

 

 


💡 느낀 점

  • 매번 이전 프로젝트 다시 열어서 코드 복사해오는 거 너무 귀찮았는데 이제 좀 편할 듯! 휴
  • theme를 만들어서 사용하는 방법은 이번에 처음 알았다. 더 간편한 것 같은데 진작에 이거 쓸걸...

📘 참고한 자료


 

 

반응형

댓글