매끄러운 사용자 경험을 위한 커스텀 BottomSheetDialogFragment 만들기

berry·2023년 10월 31일
0

해당 PR
바로가기

요구 사항

  1. 스크롤을 해도 하단에 고정되어 있는 댓글 작성 뷰를 가져야 된다.
  2. 내부 데이터 개수와 상관없이 일정한 뷰 크기를 가지는 BottomSheetDialogFragment를 만들어야 된다.

최종 결과

1. 하단 고정 뷰를 만들어보자

최종 결과

Dialog 가 collapse/expand 될 때 하단에 항상 고정되어 있는 뷰를 만들어보자

문제 상황

나는 RecyclerView 하위에 EditText 뷰를 두고, EditText 뷰가 항상 하단에 고정되어 있는 모습을 원했다.

그래서 editText 영역의 ConstraintLayout bottom to bottom 을 parent 로 해줬다.

또한 recyclerview 의 bottom 을 editText 뷰의 top 으로 설정하고, top 을 “댓글” TextView 의 bottom 으로 제약을 걸어준 후, height 를 wrap content 로 주었다. (height 을 0으로 설정하고 싶었는데 그러면 아이템이 아예 안뜨는 문제가 있다 ㅠㅠ 이 문제점도 정리하기)
내가 예상하기로는, 처음 다이얼로그가 show 될 때 화면의 반만 차지하기 때문에, EditText 가 하단에 붙고, RecyclerView 의 높이가 알아서 줄어들어 있을 줄 알았다.

그러나 내가 원하던대로 작동하지 않았다.

RecyclerView 의 높이가 알아서 줄어들지 않았고, EditText 뷰는 리사이클러뷰의 내용이 길게 표시되었기 때문에 화면에 보이지 않았다.

EditText 의 bottom 을 parent 에 제대로 제약을 걸어줬고, RecyclerView 의 Top 과 Bottom 또한 제약을 제대로 걸어줬기 때문에 왜 RecyclerView 의 높이가 조절되지 않는지 의문이었다. (이 문제로 삽질 오지게 했다.) .

그러나 실제로 LayoutInspector 를 본 결과, 처음 다이얼로그가 show 됐을 때 디스플레이 크기의 1/2 의 창 크기가 생성되는게 아니었다.

아래 캡쳐화면 처럼 전체 다이얼로그 화면의 밑 부분은 아래에 가려져 있고, dialog 를 위로 드래그 했을 때 가려져있던 전체화면이 뜨는 형식으로 동작을 하는 것이었다… 하….

드디어 해결

해결 방법은 이제 간단하다.

하단 EditText 영역을 리사이클러뷰와는 별도의 레이아웃 파일로 생성해서
Dialog 가 show 될 때 Dialog 뷰 위에 동적으로 addView 되도록 구현해주었다.
그러면 Dialog 가 접히거나 펼쳐질 때 하단 뷰가 항상 보일 것이다!

단, 이렇게 동적으로 뷰를 추가해줄 때는 주의할 점이 있다.
현재 코드에서 하단 뷰 객체를 단 한번만 생성하고 Dialog 뷰가 띄워질때마다 동적으로 추가해주고 있다.

private fun ViewGroup.addBottomFixedItemView(view: View) {
        val layoutParams = FrameLayout.LayoutParams(
            FrameLayout.LayoutParams.MATCH_PARENT,
            FrameLayout.LayoutParams.WRAP_CONTENT,
            Gravity.BOTTOM
        )
        addView(view, layoutParams)
    }

다이얼로그를 처음 열었을 때는 아무 문제 없다.
그러나 다이얼로그를 닫고 다시 열었을 때 아래와 같은 에러 로그가 찍힌다.

java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first.

해당 뷰가 이미 어떤 뷰에 add 되어있기 때문에 또 다시 add 를 할 수 없다는 오류다.
로그에서 알려주는대로 addView 하기 전에 removeView() 를 해줘야 된다.

다이얼로그가 닫혔을 때 실행되는 함수인 onCancel 에서 뷰를 제거하는게 자연스러운 것 같아 onCancel 함수에서 removeView 를 해주었다.

override fun onCancel(dialog: DialogInterface) {
        super.onCancel(dialog)
        (bottomSheetDialogParentContainer as ViewGroup).removeView(bottomFixedItemView)
    }

그러면 이제 더 이상 오류가 발생하지 않는다!

2. 일정한 뷰 크기 구현하기

최종 결과

리사이클러뷰에 아이템이 하나도 없거나, 하나만 있어도 collapse 상태에서는 최소한의 뷰 크기를 유지하고, 다이얼로그를 위로 스와이프 한다면 전체화면이 되게 해보자.

문제 상황

처음에는 Contraint Layout 의 내부 아이템의 높이와 상관없이 뷰를 일정한 비율로 유지하게 하기 위하여 app:layout_constraintDimensionRatio="W, 10:9" 를 이용하여 최소 화면 크기를 유지하도록 구현했었다.
그러나 문제점은 이렇게 화면 크기를 고정해버리면 다이얼로그가 전체 화면으로 스와이프가 안된다.

드디어 해결

BottomSheetDialogFragment 는 내부적으로 frameLayout안에 우리가 만든 dialog layout xml을 넣어서 다이얼로그를 띄운다. 그래서 다이얼로그의 parent를 참조해서 frameLayout의 크기를 코드상에서 match parent로 바꿔주니깐 전체화면으로 스와이프가 가능하다!

 private fun BottomSheetDialog.initShowListener() {
        setOnShowListener {
            bottomSheetDialogParent =
                findViewById<FrameLayout>(com.google.android.material.R.id.design_bottom_sheet)
                    ?: return@setOnShowListener
            bottomSheetDialogParentContainer =
                findViewById<FrameLayout>(com.google.android.material.R.id.container)
                    ?: return@setOnShowListener
            bottomSheetDialogParent.makeFullSize()
        }
    }

private fun View.makeFullSize() {
        this.layoutParams.height = FrameLayout.LayoutParams.MATCH_PARENT
        requestLayout()
    }
profile
공부 내용 기록

0개의 댓글