BottomSheetBehavior 파헤치기

hongbeom
8 min readAug 26, 2020

--

CoordinatorLayout에서 layout_behaviorBottomSheetBehavior로 설정해 준 경우 어떻게 동작하는지 알아봅니다.

BottomSheetBehavior

BottomSheetBehaviorCoordinatorLayout 에서 자식 뷰에 대한 플러그인 중 하나입니다. 이 옵션을 자식 뷰의 app:layout_behavior에서 설정해주면 하단에서 펼쳐지는 방식으로 자식 뷰가 동작하게 됩니다. BottomSheetDialogBottomSheetDialogFragment 도 같은 방식으로 동작합니다.

다음처럼 CoordinatorLayout에서 사용할 수 있습니다.

layout_behaviorBottomSheetBehavior로 설정해주는 것이 중요합니다. 그리고 해당 뷰를 이런 식으로 BottomSheetBehavior에 연결하여 사용할 수 있습니다.

val behavior = BottomSheetBehavior.from(
findViewById<TextView>(R.id.textView_bottom_sheet)
)

Behavior 속성

app:layout_behaviorBottomSheetBehavior를 설정했다면 다음과 같은 속성도 사용할 수 있습니다.

  • behavior_hideable : 아래로 드래그했을 때 뷰를 숨길지 여부를 결정합니다. DialogBottomSheet의 경우의 기본값은 true이고 그 외의 경우 기본값은 false입니다 (본 글에서 기본값은 false)
  • behavior_skipCollapsed: 뷰를 숨길 때 접히는 상태를 무시할 지 여부를 결정합니다. 기본값은 false이며 behavior_hideablefalse라면 효과가 없습니다.
  • behavior_draggable : 드래그하여 뷰를 접을 지 펼칠 지 여부를 결정합니다. 기본값은 true입니다.
  • behavior_fitToContents: 펼쳐진 뷰의 높이가 content를 감쌀 것인지의 여부를 결정합니다. false로 설정하게 되면 뷰가 펼쳐졌을 때 아래로 드래그할 경우 부모 컨테이너 높이의 절반으로 먼저 접히고 다시 드래그 하면 완전히 접혀집니다. 기본값은 true입니다.
  • behavior_halfExpandedRatio : 절반만 펼쳐졌을 경우 뷰의 높이를 결정합니다. 기본값은 0.5입니다. behavior_fitToContents 가 true라면 효과가 없습니다.
  • behavior_expandedOffset : 완전히 펼쳐진 상태일 때 뷰의 오프셋을 결정합니다. 마찬가지로 behavior_fitToContents 가 true라면 효과가 없으며 절반으로 접혔을 경우의 오프셋보다 커야합니다. 기본값은 0dp입니다.
  • behavior_peekHeight : 뷰가 접힌 상태의 높이입니다. 기본값은 auto입니다.

Behavior 상태

BottomSheetBehaviorstate는 다음과 같습니다.

  • STATE_EXPANDED : 완전히 펼쳐진 상태
  • STATE_COLLAPSED : 접혀있는 상태
  • STATE_HIDDEN : 아래로 숨겨진 상태 (보이지 않음)
  • STATE_HALF_EXPANDED : 절반으로 펼쳐진 상태
  • STATE_DRAGGING : 드래깅되고 있는 상태
  • STATE_SETTLING : 드래그/스와이프 직후 고정된 상태

다음과 같이 state를 조정할 수 있습니다.

behavior.state = BottomSheetBehavior.STATE_EXPANDED

Behavior 상태 저장

BottomSheetBehavior 로 설정된 뷰가 파괴되고 다시 생성되는 경우 유지할 속성들을 다음과 같은 옵션들에서 선택하여 유지할 수 있습니다.

  • SAVE_PEEK_HEIGHT : behavior_peekHeight 속성 유지
  • SAVE_HIDEABLE : behavior_hideable 속성 유지
  • SAVE_SKIP_COLLAPSED : behavior_skipCollapsed 속성 유지
  • SAVE_FIT_TO_CONTENTS : behavior_fitToContents 속성 유지
  • SAVE_ALL : 모든 속성 유지
  • SAVE_NONE : 기본값. 모든 속성 보존 X

다음처럼 사용할 수 있습니다.

behavior.saveFlags = BottomSheetBehavior.SAVE_FIT_TO_CONTENTS

BottomSheetCallback

이제 BottomSheetBehavior 의 콜백을 수신할 수 있습니다.

이 때 onSlideslideOffset 범위는 -1.0부터 1.0의 범위를 가지며 숨겨진 상태는 -1.0, 접힌 상태는 0.0, 펼쳐진 상태는 1.0입니다.

콜백을 활용하여 이런 식으로 사용할 수 있습니다.

BottomSheetBehavior.gif

내부 동작

그렇다면 어떻게 이런 동작들이 가능한 걸까요? 기본적으로 BottomSheetBehaviorCoordinatorLayout 클래스 내부의 Behavior라는 추상클래스를 상속받고 있습니다. 그래서 CoordinatorLayout에서 자식 뷰에app:layout_behavior라는 속성을 지정해줌으로써 상호작용할 수 있는 것입니다. 또한 뷰가 접힌 상태에서 레이아웃 인스펙터로 살펴보면 다음과 같은 모습을 하고 있습니다.

state — collapsed

BottomSheetBehavior로 설정된 뷰는 현재 보이지 않는 상태지만 이미 그려져있는 상태인 것을 알 수 있습니다.

BehavioronLayoutChild() 메소드를 구현한 BottomSheetBehavior 클래스 내부의 onLayoutChild()를 살펴보면 다음처럼 구현되어 있는 것을 확인할 수 있습니다.

offsetTopAndBottom()이라는 메소드로 높이를 지정해주는데 이 메소드를 살펴보면 다음과 같습니다.

먼저 24번째 라인을 살펴보면 ViewParent를 가져와서 invalidateChild를 호출함으로써 자식뷰를 그려주고 있습니다.

이 때 STATE_HIDDEN인 경우 부모 뷰의 높이 값을 가져와서 offset 인자로 넣어주었으므로 offset은 0이 아니므로 2번째 라인으로 들어와서 31번째 mRenderNode.offsetTopAndBottom(offset); 라인을 거치게 됩니다. 이 때 부모 뷰의 높이 값을 offset으로 주었으므로 offset 만큼 아래에 그려지게 됩니다.

그렇다면 STATE_COLLAPSED인 경우는 어떨까요? STATE_COLLAPSED일 경우에는 calculateCollapsedOffset() 메소드에서 계산된 offset을 가지고 자식 뷰가 그려질 위치를 계산하게 되는데 이 때 calculateCollapsedOffset() 에서는 부모 뷰의 높이와 우리가 지정한 fitToContentsOffset, peekHeight 값을 가지고 offset을 계산합니다.

Conclusion

BottomSheetBehavior 말고도 Behavior를 상속받는 다른 Behavior 관련 클래스들이 존재하니 상황에 맞게 사용하면 굉장히 유용할 것 같습니다.

읽어주셔서 감사합니다! 🙌

참고자료

--

--