chacha's

📁 Dialog Fragment 이것 저것 본문

Android/TIL

📁 Dialog Fragment 이것 저것

Cha_Cha 2021. 9. 30. 14:13

목차

     

     모서리 둥글게 하기 ( Round Corner )

    아래와 같이 커스텀 다이얼로그를 만들 때, 모서리를 둥글게 하고 싶은 경우가 있습니다.

    Dialog Fragment

    1. [ @drawable/bg_dialog ] 배경으로 사용될 drawable 파일을 생성합니다. 

    <?xml version="1.0" encoding="utf-8"?>
    <shape xmlns:android="http://schemas.android.com/apk/res/android"
        android:shape="rectangle">
        <solid android:color="@color/dialog_background" />
        <corners android:radius="10dp" />
    </shape>

    2. [ layout/dialog_custom.xml ] layout에서 Background로 지정합니다.

    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.ConstraintLayout 
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@drawable/bg_dialog"
        ...>
        ...
        <!-- 추가 요소 -->
        ...
    </androidx.constraintlayout.widget.ConstraintLayout>

    3. Custom Dialog 코드 작성

    위의 2가지 코드만 적용하면 해결될 것이라고 생각하지만, 코드에서 다이얼로그의 백그라운드를 제거해줘야 모서리가 둥글게 나옵니다.

    class CustomDialog : DialogFragment() {
    
        override fun onCreateView(
            inflater: LayoutInflater,
            container: ViewGroup?,
            savedInstanceState: Bundle?
        ): View? {
            val view = inflater.inflate(R.layout.dialog_custom, container, false)
            dialog?.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
            dialog?.window?.requestFeature(Window.FEATURE_NO_TITLE)
            ...
            return view
        }
    }

    dialog?.window?.requestFeature(Window.FEATURE_NO_TITLE)은 삽입하지 않아도 됩니다. 하지만 Android 4.4 버전 이하에서는 파란 선이 다이얼로그 상단에 나타난다고 하니 minSdk를 확인하고 작성 여부를 결정하면 될 것 같습니다.

     

     사이즈(width, height) 조절하기

    DialogFragment를 띄울 때 내가 설정한 값과 상관없이 Dialog 내용물에 따라 자동으로 뷰가 랜더링됩니다. 분명 android:layout_width="match_parent"로 선언했기 때문에 꽉 차게 랜더링될 것 같지만, 결과물을 보는 순간 내 바램이었다는 것을 깨닫게 되었습니다.

    위의 문제를 해결하기 위해서는 2가지 방법이 있습니다.


    1. 코드에서 setLayout(width, height)을 지정하는 방법 

    이상하게 xml에서 Dp로 지정한 경우에도 match_parent로 설정했던 경우와 똑같이 내용물에 맞게 뷰가 랜더링되었습니다. 이를 해결하기 위해서는 미리 지정해둔 dp 크기를 불러와서 코드상에서 dialog의 width/height를 지정해야 했습니다. 이 방법은 구글링 시 stack Overflow에 가장 빈번하게 나오는 해결 방법입니다.

    • layout/dialog_custom.xml
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    • CustomDialog.kt
        override fun onResume() {
            ...
            val width = resources.getDimensionPixelSize(R.dimen.popup_width)
            val height = resources.getDimensionPixelSize(R.dimen.popup_height)
            getDialog().getWindow().setLayout(width, height)
        	...
        }

    하지만 위의 방법은 디바이스 크기에 따라서 동일한 뷰를 보장받을 수 없다는 문제가 있습니다. 또한 height는 wrap_content로 구현하고 싶은 경우 해당 방법은 적절하지 않습니다. 


    2. 디바이스의 화면 크기를 구해서 setAttributes를 지정하는 방법

    이 방법은 가로 크기에 비례하여 지정한 비율로 레이아웃을 구상하는 경우입니다. 첫번째 방법과 달리, width의 비율만 지정하고 height는 내용물에 따라 자동으로 조절됩니다.

    • layout/dialog_custom.xml
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    • CustomDialog.kt
    class CustomDialog: DialogFragment() {
        ...
        override fun onResume() {
            super.onResume()
            // 디바이스 size 구하기
            val params: ViewGroup.LayoutParams? = dialog?.window?.attributes
            val windowManager = activity?.getSystemService(Context.WINDOW_SERVICE) as WindowManager
            val size = windowManager.currentWindowMetricsPointCompat() // 디바이스 가로,세로 길이
            val deviceWidth = size.x // 디바이스 가로 길이
            
            params?.width = (deviceWidth * 0.9).toInt() // 여백 비율을 지정
            dialog?.window?.attributes = params as WindowManager.LayoutParams
        }
    }

    DialogFragment의 onResume()에서 실행해야 합니다.

        fun WindowManager.currentWindowMetricsPointCompat(): Point {
            return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
                val windowInsets = currentWindowMetrics.windowInsets
                var insets: Insets = windowInsets.getInsets(WindowInsets.Type.navigationBars())
                windowInsets.displayCutout?.run {
                    insets = Insets.max(
                        insets,
                        Insets.of(safeInsetLeft, safeInsetTop, safeInsetRight, safeInsetBottom)
                    )
                }
                val insetsWidth = insets.right + insets.left
                val insetsHeight = insets.top + insets.bottom
                Point(
                    currentWindowMetrics.bounds.width() - insetsWidth,
                    currentWindowMetrics.bounds.height() - insetsHeight
                )
            } else {
                Point().apply {
                    defaultDisplay.getSize(this)
                }
            }
        }

     

     🧶 DialogFragment에 ViewBinding 사용하기

    How to correctly use Android View Binding in DialogFragment? - stack overflow

    View를 직접 Inflate 하는 대신에 binding.root를 사용하여 뷰를 설정해주면 됩니다. DialgoFragment에서 뷰바인딩(View Binding)을 사용하는 것도 Fragment에서 사용하는 것과 같이 onDestroyView()에서 해제해주어야 합니다. ( 참고: Use view binding in fragments - Docs )

    class CustomDialog : DialogFragment() {
    
        private var _binding: DialogCustomBinding? = null
        // This property is only valid between onCreateDialog and
        // onDestroyView.
        private val binding get() = _binding!!
    
        override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
            _binding = DialogCustomBinding.inflate(LayoutInflater.from(context))
            return AlertDialog.Builder(requireActivity())
                .setView(binding.root)
                .create()
        }
        
        override fun onDestroyView() {
            super.onDestroyView()
            _binding = null
        } 
    }

     

     🥾 DialogFragment로부터 Click Event 전달받기

     Passing Events Back to the Dialog's Host - Doc
     How to get data from DialogFragment to a Fragment? - stack overflow

    Dialog의 어떤 버튼이 클릭되었는지 Dialog를 호출한 곳에서 알 수 있는 방법입니다.

    1. DialogFragment 선언하기

    class StopDialog : DialogFragment() {
        // Dialog의 Host에게 Event를 전달하기 위한 리스너
        private lateinit var listener: StopDialogListener
    
        /* Activity에서 아래의 interface를 구현해주어야 합니다. 
         * 각 메서드는 Dialog를 파라미터로 전달합니다. 
         */
        interface StopDialogListener {
            fun onDialogPositiveClick(dialog: DialogFragment)
            fun onDialogNegativeClick(dialog: DialogFragment)
        }
    
        private lateinit var positiveButton: TextView
        private lateinit var negativeButton: TextView
    
        override fun onAttach(context: Context) {
            super.onAttach(context)
            try {
                // 리스너를 인스턴스화함으로서 host에 event를 보냄
                listener = context as StopDialogListener
            } catch (e: ClassCastException) {
                // Activity에 interface가 구현되지 않았으면 Excetion thorw
                throw ClassCastException(
                    (context.toString() +
                            " must implement NoticeDialogListener")
                )
            }
        }
    
        override fun onCreateView(
            inflater: LayoutInflater,
            container: ViewGroup?,
            savedInstanceState: Bundle?
        ): View? {
            ...
            positiveButton.setOnClickListener {
                listener.onDialogPositiveClick(this)
            }
            negativeButton.setOnClickListener {
                listener.onDialogNegativeClick(this)
            }
            ...
        }
    }

    2. Activity에서 Listener 선언하기

    class RecordActivity : AppCompatActivity(), StopDialog.StopDialogListener {
        /**
         * Record Stop And Save Dialog
         **/
        private fun showSaveDialog() {
            val dialog = StopDialog()
            dialog.show(supportFragmentManager, "StopDialog")
        }
    
        override fun onDialogPositiveClick(dialog: DialogFragment) {
                Toast.makeText(dialog.context, "Positive clicked!", Toast.LENGTH_SHORT).show()
                dismiss()
        }
    
        override fun onDialogNegativeClick(dialog: DialogFragment) {
                Toast.makeText(dialog.context, "Negative clicked!", Toast.LENGTH_SHORT).show()
                dismiss()
        }
    }

     Fragment에서 DialogFragment의 Click Event 전달 받기

     Custom dialog interface for multiple Activities/ Fragments - stack overflow
     DialogFragment Listener - stack overflow

    위의 Acitivity에서 전달 받는 것과 유사하지만, onAttach 부분을 수정하지 않으면 Activity에 리스너를 선언하지 않았다는 오류를 만나게 됩니다. onAttach 부분을 아래와 같이 변경하면 Fragment에 리스너가 전달되는 것을 확인할 수 있습니다.

        override fun onAttach(context: Context) {
            super.onAttach(context)
            try {
                listener = parentFragment as StopDialogListener
            } catch (e: ClassCastException) {
                ...
            }
        }

    이외에도 동일 DialogFragment를 Activity와 동시에 사용할 수도 있습니다. parentFragment는 Acitivity에서 호출한 경우, null을 반환합니다. 이를 이용하여 아래와 같이 선언 시, Activity에서 호출하는 경우와 Fragment에서 호출하는 경우로 나눠서 리스너를 처리할 수 있습니다.

        override fun onAttach(context: Context) {
            super.onAttach(context)
            try {
                listener = parentFragment?.let {
                    parentFragment as NoticeDialogListener
                } ?: context as NoticeDialogListener
            } catch (e: ClassCastException) {
                ...
            }
        }

    End

    Comments