본문 바로가기
깡샘 코틀린

10-3 다양한 다이얼로그

by 농농씨 2023. 6. 24.

다이얼로그(dialog)란 사용자와 상호 작용하는 대화상자이다. 여기서는 가장 많이 사용하는 토스트, 날짜 또는 시간 입력, 알림 창 등을 알아보고 개발자가 직접 구성하는 커스텀 다이얼로그도 살펴보겠다.

 

 

토스트 메시지 띄우기

토스트(Toast)는 화면 아래쪽에 잠깐 보였다가 사라지는 문자열을 의미하며 사용자에게 간단한 메시지로 특정한 상황을 알릴 때 사용한다.

토스트를 사용하는 대표적 예로 사용자가 폰의 뒤로가기 버튼을 눌러 앱이 종료될 때 "종료하려면 한 번 더 누르세요."를 띄울 수 있다. 이 밖에 간단한 예외 메시지를 출력할 때도 토스트를 많이 사용한다.

토스트는 Toast의 makeText() 함수로 만든다.

  • open static fun makeText(context: Context!, text: CharSequence!, duration: Int)" Toast!
  • open static fun makeText(context: Context!, resId: Int, duration: Int): Toast!

makeText() 함수의 두 번째 매개변수는 2️⃣출력할 문자열이며,(첫번째는 뭔데?) 세 번째 매개변수는 3️⃣토스트가 화면에 출력되는 시간으로, 보통 다음의 상수를 사용한다. Toast.LENGTH_SHORT는 일반적으로 3초 정도이며 Toast.LENGTH_LONG은 5초 정도의 시간을 의미한다. 토스트가 화면에 출력된 후 이 시간이 지나면 자동으로 사라진다.

  • val LENGTH_LONG: Int
  • val LENGTH_SHORT: Int
// 토스트 출력 예
val toast = Toast.makeText(this, "종료하려면 한 번 더 누르세요", Toast.LENGTH_SHORT)
toast.show()

makeText() 함수로 만든 토스트는  show() 함수로 화면에 출력한다. 그런데 토스트는 makeText() 함수를 이용하지 않고 다음의 세터 함수로 만들어질 수도 있다. 

  • open fun setDuration(duration: Int): Unit
  • open fun setGravity(gravity: Int, xOffset: Int, yOffset: Int): Unit
  • open fun setMargin(horizotalMargin: Float, verticalMargin: Float): Unit
  • open fun setText(resId: Int): Unit

setDuration(), setText() 함수를 이용하면 문자열이나 화면에 보이는 시간을 설정할 수 있다. 그리고 setGravity() 나 setMargin() 함수를 이용하면 토스트가 뜨는 위치를 정할 수도 있다.

 

토스트는 간단한 문자열을 화면에 출력하는 목적이므로 show() 함수로 출력한 후 일정한 시간이 지났을 때 자동으로 사라지게 만들면 된다. 그런데 토스트가 화면에 보이거나 사라지는 순간을 콜백으로 감지해 특정 로직을 수행하게 할 수도 있다. 이 콜백 기능은 API 레벨 30 버전에서 추가되었다. 따라서 콜백 기능을 이용한다면 API 레벨 호환성을 고려해 작성해야 한다.

// 콜백 기능 이용하기
@RequiresApi(Build.VERSION_CODES.R)
fun showToast() {
	val toast = Toast.makeText(this, "종료하려면 한 번 더 누르세요.", Toast.LENGTH_SHORT)
    toast.addCallback( // 토스트의 콜백을 등록함
    	object : Toast.Callback() {
        	override fun onToastHidden() { // 토스트가 화면에서 사라지는 순간에 호출됨(재정의 함수)
            	super.onToastHidden()
                Log.d("kkang", "toast hidden")
            }
            
            override fun onToastShown() { // 토스트가 화면에 나타나는 순간에 호출됨
            	super.onToastShown()
                Log.d("kkang", "toast shown")
            }
        })
    toast.show()
}

토스트의 콜백을 등록하려면 Toast.Callback 타입의 객체를 토스트 객체의 addCallback() 함수로 등록해주면 된다. 이렇게 하면 화면에 토스트가 뜨는 순간 자동으로 콜백 객체의 onToastShown() 함수가 호출되며, 화면에서 사라지는 순간 onToastHidden() 함수가 자동으로 호출된다.

 

 

날짜 또는 시간 입력받기

앱에서 날짜나 시간을 입력받는 데 사용하는 다이얼로그피커(Picker) 다이얼로그라고 한다. 날짜를 입력 받을 때는 데이트 피커 다이얼로그(Date Picker Dialog)를, 시간을 입력받을 때는 타임 피커 다이얼로그(Time Picker Dialog)를 사용한다.

날짜를 입력받는 데이트 피커 다이얼로그의 생성자는 다음과 같다.

// 데이트 피커 다이얼로그 생성자
DatePickerDialog(context: Context, 
	listener: DatePickerDialog.OnDateSetListener?, year: Int, month: Int, 
    dayOfMonth: Int)

(한눈에 보려고 임의로 줄 나눔)

두 번째 매개변수로 DatePickerDialog.OnDateSetListener 구현 객체를 등록하면 다이얼로그에서 사용자가 설정한 날짜를 콜백 함수로 얻을 수 있다. 나머지 Int 타입의 매개변수는 처음에 보이는 날짜입니다. month 값은 0부터 11까지 지정되며 이때 0은 1월을 의미한다.

// 데이트 피커 다이얼로그 사용 예
DatePickerDialog(this, object: DatePickerDialog.OnDateSetListener {
	override fun onDateSet(p0: DatePicker?, p1: Int, p2: Int, p3: Int) {
    	Log.d("kkang", "year : $p1, month : ${p2+1}, dayOfMonth : $p3)
    }
}, 2020, 8, 21).show()

 

시간을 입력받는 타임 피커 다이얼로그의 생성자는 다음과 같다.

// 타임 피커 다이얼로그 생성자
TimePickerDialot(context: Context!, listener: TimePicker/dialog.OnTimeSetListener!,
hourOfDay: Int, minute: Int, is24HoutView: Boolean)

두 번째 매개변수로 TimePickerDialog.OnTimeSetListener를 구현한 객체를 지정하면 사용자가 다이얼로그에서 설정한 시간을 얻을 수 있으며, 처음에 보일 시간을 Int 타입으로 설정할 수 있다.

그리고 마지막 매개변수로 시간을 24시간과 12시간 형태 중에 어떤 것으로 출력할 것인지를 지정한다. false로 지정해 12시간 형태로 출력하면 오전/오후를 선택하는 부분이 보인다. 반면에 true로 지정해 24시간 형태로 출력하면 오전/오후를 선택하는 부분이 보이지 않는다.

// 타임 피커 다이얼로그 사용 예
TimePickerDialog(this, object: TimePickerDialog.OnTimeSetListener { // 람다함수인듯?
	override fun onTimeSet(p0: TimePicker?, p1: Int, p2: Int) {
    	Log.d("kkang", "time : $p1, minute : $p2")
    }
}, 15, 0, true).show()

 

 

 

알림 창 띄우기

안드로이드 다이얼로그의 기본은 이 책에서 알림 창으로 부르는 AlertDialog이다. 알림 창은 단순히 메시지만 출력할 수도 있고 다양한 화면을 출력할 수도 있다. 사실 앞에서 살펴본 데이트 피커와 타임 피커도 AlertDialog의 하위 클래스이며 각각의 화면에 데이트 피커와 타임 피커를 출력한 다이얼로그이다.(먼말?)

알림창은 크게 3가지 영역으로 구분된다. 위부터 차례대로 제목, 내용, 버튼 영역이 있다.

ex) 😀text dialog "정말 종료하시겠습니까?" cancel/ok

그런데 이 3가지 영역이 항상 보이는 것은 아니다. 즉, 알림 창을 설정할 때 제목과 버튼 정보를 지정하지 않았다면 내용 영역만 나온다. 

ex. 정말 종료하시겠습니까?

 

알림 창의 생성자는 접근 제한자가 protected로 선언돼서 객체를 직접 생성할 수 없다. 그 대신 AlertDialog.Builder를 제공하므로 이 빌더를 이용해 알림창을 만든다. 먼저 AlertDialg.Builder를 생성하고 빌더의 세터 함수로 알림 창의 정보를 지정한다.

// 알림 창 빌더
AlertDialog.Builder(context: Context!)

 

다음은 알림 창에 아이콘과 제목, 내용을 지정하는 함수이다.

  • open fun setIcon(iconId: Int): AlertDialog.Builder!
  • open fun setTitle(title: CharSequence!): AlertDialog.Builder!
  • open fun setMessage(message: CharSequence!): AlertDialog.Builder!

setIcon() 함수는 제목 영역에 아이콘을 출력하며 setTitle() 함수는 제목 문자열을 출력한다. 내용 영역에 간단한 문자열을 출력할 때는 setMessage() 함수를 사용한다. 

 

다음은 알림 창에 버튼을 지정하는 함수이다.

  • open fun setPositiveButton(text: CharSequence!, listener: DialogInterface.OnClickListener!): AlertDialog.Builder!
  • open fun setNegativeButton(text: CharSequence!, listener: DialogInterface.OnClickListener!): AlertDialog.Builder!
  • open fun setNeutralButton(text: CharSequence!, listener: DialogInterface.OnClickListener!): AlertDialog.Builder!

각 함수의 1️⃣첫 번째 매개변수는 버튼의 문자열이며 2️⃣두 번째 매개변수는 사용자가 버튼을 클릭했을 때 처리할 이벤트 핸들러*이다. 만약 버튼을 클릭했을 때 처리할 내용이 없다면 두 번째 매개변수에 null을 대입한다. 이렇게 null을 대입하더라도 버튼을 클릭하면 창이 닫힌다.

*이벤트 핸들러란?

  • 이벤트 소스: 이벤트가 발생한 객체
  • 이벤트 핸들러: 이벤트 발생 시 실행할 로직이 구현된 객체

 

알림 창의 버튼은 최대 3개까지만 추가할 수 있다. 만약 같은 함수를 여러 번 사용하면 버튼은 중복되어 하나만 나타난다.

// 알림 창 띄우기
AlertDialog.Builder(this).run {
	setTitle("test dialog")
    setIcon(android.R.drawable.ic_dialog_info)
    setMessage("정말 종료하시겠습니까?")
    setPositiveButton("OK", null)
    setNegativeButton("Cancel", null)
    setNeutralButton("More", null)
    setPositiveButton("Yes", null)
    setNegativeButton("No", null)
    show()
}

버튼 함수를 setPositiveButton(), setNegativeButton(), setNeutralButton()으로 구분하는 이유는 이벤트 핸들러에서 어떤 버튼이 클릭되었는지 구분하기 위해서이다. 각 이벤트에 해당하는 이벤트 핸들러를 따로 만들 수도 있지만, 한 알림 창의 버튼 이벤트를 하나의 이벤트 핸들러에서 모두 처리할 수도 있다. 이때 어떤 버튼이 클릭되었는지를 구분해야 하는데, 셋 중 어떤 함수를 사용했는지에 따라 이벤트 핸들러에 전달되는 매개변숫값이 달라서 그 값으로 구분한다.

 

// 버튼의 이벤트 핸들러 등록
val eventHandler = object : DialogInterface.OnClickListener {
    override fun onClick(p0: DialogInterace?, p1: Int) { // positive 버튼이면
    	if (p1 == DialogInterface.BUTTON_POSITIVE) {
        	Log.d("kkang", "positive button click")
        } else if (p1 == DialogInterface.BUTTON_NEGATIVE) { // negative 버튼이면
        	Log.d("kkang", "negative button click")
        }
    }
}
(...생략...)
setPositiveButton("OK", eventHandler)
setNegativeButton("Cancel", eventHandler)

알림 창을 클릭했을 때 호출되는 onClick() 함수의 두 번째 매개변수(여기서는 p1)가 이벤트가 발생한 버튼을 알려 준다. setPositiveButton() 함수로 만든 버튼은 이벤트 구분자가 Dialoginterface.BUTTON.POSITIVE로 지정되며, setNegativeButton() 함수로 만든 버튼은 이벤트 구분자가 DialogInterface.BUTTON_NEGATIVE로 지정된다. 따라서 이 값으로 버튼을 구분해 적절하게 처리해 주면 된다.

 

알림 창의 내용 영역에는 간단한 문자열을 출력하는 setMessage() 말고도 다양한 함수가 있다. 만약 목록을 제공하고 이 중 하나를 선택받는 알림 창을 만들고자 한다면 setItems(), setMultiChoiceItems(), setSingleChoiceItems() 함수를 이용한다. 함수에서 첫 번째 매개변수는 배열 정보이며 이곳의 문자열이 목록에 출력된다.

  • open fun setItems(items: Array<CharSequence!>!, listener: DialogInterface.OnClickListener!): AlertDialog.Builder!
  • open fun setMultiChoiceItems(items: Array<CharSequence!>!, checkedItems: BooleanArray!, listener: DialogInterface.OnMultiChoiceClickListener!): AlertDialog.builder!
  • open fun setSingleChoiceItems(items: Array<CharSequence!>!, checkedItem: Int, listener: DialogInterface.OnClickListener!): AlertDialogBuilder!
// 목록을 출력하는 알림 창
val items = arrayOf<String>("사과", "복숭아", "수박", "딸기") // 출력할 문자열의 배열 선언
AlertDialog.Builder(this).run { // 알림창은 직접 객체 생성하지 못하고 빌더를 이용해 알림창 만듦
	setTitle("items test") // 알림창은 빌더의 새터 함수로 알림창의 정보 지정함
    setIcon(android.R.drawablc.ic_dialog_info) // 알림창 아이콘 지정함
    setItems(items, object: DialogInterface.OnClickListener { // 문자열 목록 출력하는 세터함수
    	override fun onClick(p0: DialogInterface?, p1: Int) {
        	Log.d("kkang", "선택한 과일 : ${items[p1]}")
        }
    })
    setPositiveButton("닫기", null)
    show()
}

setItems() 함수의 두 번째 매개변수는 항목을 선택할 때의 이벤트 핸들러(복습: 이벤트 처리하는 로직)이며 사용자가 항목을 선택하면 onClick() 함수가 자동으로 호출된다. 사용자가 선택한 항목의 인덱스는 onClick() 함수의 두 번째 매개변수로 전달된다.

 

setMultiChoiceItems() 함수는 다중 선택을 위한 체크박스가 함께 출력되는 항목을 만들어준다. 두 번째 매개변수로 처음 체크 상태를 지정한다.

// 체크박스를 포함하는 예
setMultiChoiceItems(items, booleanArrayOf(true, false, true, false), // 첫번째,세번째 항목은 처음부터 체크되어 있음
	object: DialogInterface.OnMultiChoiceClickListener {
	override fun onClick(p0: DialogInterface?, p1: Int, p2: boolean) {
    	Log.d("kkang", "${items[p1]}이 ${if(p2) "선택되었습니다." else "선택 해제되었습니다."}")
    }
})

setMultiChoiceItems() 함수의 세 번째 매개변수항목을 선택할 때의 이벤트 핸들러이며 사용자가 항목을 선택하는 순간 onClick() 함수가 자동으로 호출된다. onClick() 함수의 두 번째 매개변수로 선택한 항목의 인덱스가 전달되며, 세 번째 매개변수로 체크 상태가 전달된다.

 

❓Log.d() 함수의 두 번째 매개변숫값이 문자열인데 if~else 문이 사용되었어요. 이게 가능한 코드인가요?

❗️코틀린의 if~else 문은 자바와 다르게 표현식으로도 사용할 수 있다. 표현식이란 실행결과가 발생하는 구문을 의미하며 if~else 문도 결괏값이 발생하는 구문으로 사용할 수 있다.

val result = if (10>11) "true입니다." else "false입니다."

이와 같이 if~else 문을 작성하면 실행에 의한 결괏값이 발생하며 그 값이 변수에 대입된다. 위의 result 변수에는 "false입니다."라는 문자열이 대입된다.

 

 

setSingleChoiceItems() 함수는 하나만 선택할 수 있는 라디오 버튼(체크박스는 다중선택, 라디오버튼은 동그란 버튼이고 하나만 선택가능)으로 구성된 항목을 만들어 준다. 두 번째 매개변수로 처음 선택할 항목을 지정한다.

// 라디오 버튼을 포함하는 예
setSingleChoiceItems(items, 1, object: DialogInterface.OnClickListener { // 처음에 두번째 항목 선택돼있음
	override fun onclick(p0: DialogInterface?, p1: Int) {
    	Log.d("kkang", "${items[p1]} 이 선택되었습니다.")
    }
})

 

알림 창의 제목, 내용, 버튼을 구성하는 함수 이외에 속성을 설정하는 함수를 사용할 수도 있다.

  • open fun setCancelable(cancelable: Boolean): AlertDialog.Builder!
  • open fun setCanceledOnTouchOutside(cancel: Boolean): Unit

두 함수 모두 사용자의 행동에 따라 알림 창을 닫을 것인지를 설정한다. setCancelable() 함수는 사용자가 기기의 뒤로가기 버튼을 눌렀을 때, setCanceledOnTouchOutside() 함수는 알림 창의 바깥 영역을 터치했을 때 매개변수가 true이면 닫고 false이면 닫지 않는다. 기본값은 true이다.

// 알림 창을 닫는 설정(뒤로가기, 바깥 영역 터치)
AlertDialog.Builder(this).run {
	setTitle("items test")
    setIcon(android.R.drawable.ic_dialog_info)
    setItems(items, object: DialogInterface.OnClickListener {
    	override fun onClick(p0: DialogInterface?, p1: Int) {
        	Log.d("kkang", "선택한 과일 : ${items[p1]}")
        }
    })
    setCancelable(false) // 뒤로가기 누르면 닫지 않는다(??)
    setPositiveButton("닫기", null)
    show()
}.setCanceledOnTouchOutside(false) // 알림창 바깥 터치해도 안닫는다

 

❓왜 setCancelable(false) 구문은 run{} 안에 작성하고 setCanceledOnTouchOutside(false) 구문은 run{} 밖에 작성하나요?

❗️두 함수 모두 알림 창의 속성을 설정하지만 setCancelable()AlertDialog.Builder 클래스의 함수이고, setCanceledOnTouchOutside()Dialog 클래스의 함수이기 때문이다.

run()은 매개변수에 람다 함수를 지정할 수 있는 코틀린의 기초 함수이다. 그런데 run() 함수에 지정한 람다 함수는 run()을 호출한 객체의 멤버가 되어 람다 함수에서 this는 Builder 객체를 가리킨다. 그리고 run() 함수의 반환값은 곧 람다 함수의 반환값이다.

위 소스에서는 run() 함수에 지정한 람다 함수의 마지막 줄에 show() 함수를 호출했다. show() 함수의 반환값은 AlertDialog 객체이며 이를 run() 함수가 반환하므로 AlertDialog.Builder(this).run{}.setCanceledOnTouchOutside(false)처럼 작성할 수 있다.()

 

 

커스텀 다이얼로그 만들기

다이얼로그를 만들다보면 개발자가 원하는 형태로 창을 구성하고 싶을 때도 있다. 이를 커스텀 다이얼로그라고 한다. 이 커스텀 다이얼로그도 AlertDialog를 이용한다.

그 전에 먼저 LayoutInflater라는 클래스를 이해해야 한다. 이 클래스는 커스텀 다이얼로그뿐만 아니라 다양한 곳에서 이용되므로 잘 알아두도록 하자.

LayoutInflater 클래스는 레이아웃 XML 파일을 코드에서 초기화(흔히 전개라고도 표현)하는 기능을 제공한다. 여기서 초기화란 XML 파일에 선언한 뷰를 코드에서 이용하고자 생성하는 작업을 의미한다. XML 파일은 텍스트 파일일 뿐이며 결국 코드에서 이용하려면 XML에 선언한 대로 객체를 생성해서 메모리에 할당해야 한다. 이 작업을 LayoutInflater가 해준다.

 

❓이제껏 레이아웃 XML 파일에 화면을 구성하고 액티비티에서 출력할 때 setContentView() 함수를 이용했는데, 굳이 LayoutInflater로 객체를 생성해야 하나요?

❗️액티비티의 화면을 구성하는 레이아웃 XML 파일이라면 LayoutInflater가 아니라 setContentView() 함수를 이용하면 된다. 그런데 앱을 개발하다 보면 커스텀 다이얼로그를 위한 XML 파일뿐만 아니라 이후에 나오는 리스트 뷰, 리사이클러 뷰의 항목 화면, 프래그먼트를 위한 XML 파일 등 액티비티의 화면을 목적으로 하지 않는 레이아웃 XML 파일을 많이 만든다. 이때 LayoutInflater를 이용한다.

 

LayoutInflater로 레이아웃 XML 파일을 초기화하는 작업은 어렵지 않다. 우선 getSystemService() 함수로 LayoutInflater를 얻는다. 그리고 inflate() 함수를 호출하면서 초기화할 레이아웃 XML 파일 정보를 매개변수로 전달한다.

// XML 파일 초기화
val inflater = getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater 
// LayoutInflater 종류의 클래스를 얻음(?)
val rootView = inflater.inflate(R.layout.activity_one, null) // inflater 클래스의 객체 얻음
// 동시에 XML 파일 정보를 매개변수로 넘김

inflate() 함수의 반환값초기화된 XML 의 루트 태그에 해당하는 객체이다. 만약 XML 파일의 루트 태그가 <LinearLayout>이라면 LinearLayout 객체를 반환한다.

그런데 만약 뷰 바인딩 기법을 이용한다면 XML 초기화 코드를 조금 더 쉽게 작성할 수 있다. 위 코드를 다음처럼 뷰 바인딩으로 작성해도 된다.

// 뷰 바인딩을 적용한 XML 파일 초기화
val binding = ActivityOneBinding.inflate(layoutInflater)
val rootView = binding.root

(글쿤...)

초기화할 XML에 해당하는 바인딩 클래스의 inflate() 함수를 호출하면서 매개변수로 layoutInflater 객체를 전달만 해주면 자동으로 초기화되고 루트 뷰 객체를 얻을 수 있다.

 

이제 커스텀 다이얼로그를 만드는 법을 알아보자. 먼저 다이얼로그를 구성하는 레이아웃 XML 파일을 만들어야 한다. res/layout 폴더에 dialog.input.xml 파일을 다음처럼 작성했다고 가정하자.

// 커스텀 다이얼로그 구성
<LinearLayout
	xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    <EditText
    	android:layout_width="match_parent"
        android:layout_height="wrap_content" />
    <RadioGroup
    	android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal">
        <RadioButton
        	android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="male" />
        <RadioButton
        	android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="female" />
    </RadioButton>
</LinearLayout>

이제 이 XML 파일을 LayoutInflater로 초기화해서 다이얼로그에 적용하면 된다. AlertDialog의 setView() 함수에 매개변수로 뷰 객체를 전달하면 창의 내용 영역에 출력된다.

// 커스텀 다이얼로그 출력
val dialogBinding  = DialogInputBinding.inflate(layoutInflater)
AlertDialog.Builder(this).run {
	setTitle("Input")
    setView(dialogBinding.root) // 함수에 뷰 객체 전달함
    setpositiveButton("닫기", null)
    show()
}

 

 

 

 

 

공부 메모

더보기

앞으로는 ;, :, ', "같은 기호는 자주 쓰니까 왼손 쉬프트를 이용해서 쓰는 게 더 빠를 것 같다.

아... 이 챕터 타이핑 짱많아 휴

근데 다 이해되지도 않음 어쩔수없지

단축키 메모

command+shift+delete 하면 그 줄 다 지워짐

'깡샘 코틀린' 카테고리의 다른 글

10-5 알림 띄우기  (0) 2023.06.27
10-4 소리와 진동 알림  (0) 2023.06.25
10-2 퍼미션 설정하기  (0) 2023.06.24
10-1 API 레벨 호환성 고려하기  (2) 2023.06.24
09-3 폰 크기의 호환성  (0) 2023.06.23