카카오톡 채팅화면의 키보드와 옵션창 구현 내용 정리
개발 과정
1. 레이아웃 구성
2. 로직 적용
3. 부드러운 모션 적용
1. 레이아웃 구성
채팅 리사이클러뷰 하단에 height="0dp"로 옵션창 추가
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/extra"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@color/systemGrey06"
android:paddingHorizontal="4dp"
android:paddingVertical="36dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:layout_height="0dp">
<include
android:id="@+id/ib_album"
layout="@layout/item_extra"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:image="@{@drawable/ic_extra_album}"
app:layout_constraintEnd_toStartOf="@id/ib_camera"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:text="@{`앨범`}" />
<include
android:id="@+id/ib_camera"
layout="@layout/item_extra"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:image="@{@drawable/ic_extra_camera}"
app:layout_constraintEnd_toStartOf="@id/ib_video"
app:layout_constraintStart_toEndOf="@id/ib_album"
app:layout_constraintTop_toTopOf="parent"
app:text="@{`카메라`}" />
<include
android:id="@+id/ib_video"
layout="@layout/item_extra"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:image="@{@drawable/ic_extra_video}"
app:layout_constraintEnd_toStartOf="@id/ib_capture"
app:layout_constraintStart_toEndOf="@id/ib_camera"
app:layout_constraintTop_toTopOf="parent"
app:text="@{`동영상`}" />
<include
android:id="@+id/ib_capture"
layout="@layout/item_extra"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="invisible"
app:image="@{@drawable/ic_extra_capture}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/ib_video"
app:layout_constraintTop_toTopOf="parent"
app:text="@{`캡처`}" />
</androidx.constraintlayout.widget.ConstraintLayout>
( 이게 하단 옵션 창 )
2. 로직 적용
기본 세팅
WindowInsets을 사용하기 위해 시스템 Inset을 제거 한다. ( 내가 컨트롤 할 예정 )
override fun onCreate(savedInstanceState: Bundle?) {
WindowCompat.setDecorFitsSystemWindows(window, false)
super.onCreate(savedInstanceState)
}
그리고 키보드가 올라왔을때와 시스템바에 대한 패딩을 준다 ( 전체 레이아웃 root에 )
ViewCompat.setOnApplyWindowInsetsListener(root){ _,insets ->
val mergedInset = insets.getInsets(
WindowInsetsCompat.Type.ime() or
WindowInsetsCompat.Type.systemBars()
)
root.updatePadding(
top = mergedInset.top,
left = mergedInset.left,
bottom = mergedInset.bottom,
right = mergedInset.right
)
insets
}
그 다음엔 옵션창의 높이를 키보드의 높이와 맞춰야 하는데 키보드의 높이를 알 방법이 없다.
알기 위해선 키보드를 show하고 그 값을 저장 해야 한다.
전역 변수로 keyboardTop을 만들고 추가적으로 현재 상태값도 추가한다.
private var keyboardTop = 0
private var currentImeState: ImeState = ImeState.HIDE
enum class ImeState {
HIDE, KEYBOARD, OPTION
}
키보드가 보일때 keyboardTop에 키보드의 높이를 저장해놓는다.
중요한 점 : 키보드가 올라올때는 키보드만 최하단에서 올라온다. ( 시스템바에 영향 X)
그래서 rootPadding 처리는 mergedInset.bottom 을 사용하고 keyboard 만의 높이를 가져오기 위해 시스템바 bottom을 뺐다.
ViewCompat.setOnApplyWindowInsetsListener(root){ _,insets ->
val mergedInset = insets.getInsets(
WindowInsetsCompat.Type.ime() or
WindowInsetsCompat.Type.systemBars()
)
systembarBottom = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom
if (isVisibleKeyboard) {
if (keyboardTop == 0) {
keyboardTop = mergedInset.bottom - systembarBottom
viewDataBinding.extra.updateLayoutParams<ViewGroup.LayoutParams> {
height = keyboardTop
}
}
viewDataBinding.extra.isGone = true
viewDataBinding.ibPlus.setImageDrawable(plusImage)
currentImeState = ImeState.KEYBOARD
} else if (!isVisibleKeyboard && extra.isGone) {
currentImeState = ImeState.HIDE
}
root.updatePadding(
top = mergedInset.top,
left = mergedInset.left,
bottom = mergedInset.bottom,
right = mergedInset.right
)
insets
}
isVisibleKeyboard 는 키보드가 열려져있는지 확인하는 프로퍼티다
private val isVisibleKeyboard: Boolean
get() = ViewCompat.getRootWindowInsets(viewDataBinding.root)
?.isVisible(WindowInsetsCompat.Type.ime()) == true
그리고 상태에 따라 UI를 처리하는 함수를 만든다.
private fun statusAction(status: ImeState) {
when (status) {
ImeState.HIDE -> {
hideKeyboard()
viewDataBinding.extra.isGone = true
viewDataBinding.ibPlus.setImageDrawable(plusImage)
currentImeState = ImeState.HIDE
}
ImeState.KEYBOARD -> {
if (!isVisibleKeyboard) {
showKeyboard()
}
viewDataBinding.ibPlus.setImageDrawable(plusImage)
viewDataBinding.extra.isGone = true
currentImeState = ImeState.KEYBOARD
}
ImeState.OPTION -> {
lifecycleScope.launch {
if (keyboardTop == 0) {
showKeyboard()
}
if (isVisibleKeyboard) {
hideKeyboard()
}
viewDataBinding.ibPlus.setImageDrawable(closeImage)
viewDataBinding.extra.isVisible = true
currentImeState = ImeState.OPTION
}
}
}
}
일단 중요한 로직은 OPTION 부분인데
케이스 분리를 하면
1. 처음 + 를 눌러 옵션창이 켜지는 경우
2. 처음 이후 +를 눌러 옵션창이 켜지는 경우
3. 키보드가 열려있는 상태에서 +를 눌러 옵션창이 보이는 경우
그리고 이벤트 연결
ibPlus.setOnClickListener {
statusAction(
when (currentImeState) {
ImeState.HIDE, ImeState.KEYBOARD -> ImeState.OPTION
ImeState.OPTION -> ImeState.KEYBOARD
}
)
}
3. 부드러운 모션 적용
그런데 이렇게 하면 문제가 발생한다.
키보드를 show, hide 하는 과정은 비동기기 때문에 다른 UI와 동시에 변경할때 이상현상이 생긴다.
그래서 키보드동작들을 동기로 만든다.
동기로 만든다는 의미는 showKeyboard -> keyboard show Success -> 다음 동작을 의미한다. ( 콜백 받고 처리한다는 얘기 )
suspend fun View.awaitLayoutChanged() = suspendCancellableCoroutine { cont ->
val listener = object : View.OnLayoutChangeListener {
override fun onLayoutChange(
v: View?,
left: Int,
top: Int,
right: Int,
bottom: Int,
oldLeft: Int,
oldTop: Int,
oldRight: Int,
oldBottom: Int
) {
v?.removeOnLayoutChangeListener(this)
cont.resumeWith(Result.success(Unit))
}
}
cont.invokeOnCancellation { removeOnLayoutChangeListener(listener) }
addOnLayoutChangeListener(listener)
}
뷰의 콜백을 받는 원샷 suspend 함수를 만들고
private fun statusAction(status: ImeState) {
when (status) {
ImeState.HIDE -> {
hideKeyboard()
viewDataBinding.extra.isGone = true
viewDataBinding.ibPlus.setImageDrawable(plusImage)
currentImeState = ImeState.HIDE
}
ImeState.KEYBOARD -> {
lifecycleScope.launch {
if (!isVisibleKeyboard) {
showKeyboard()
viewDataBinding.root.awaitLayoutChanged()
}
viewDataBinding.ibPlus.setImageDrawable(plusImage)
viewDataBinding.extra.isGone = true
currentImeState = ImeState.KEYBOARD
}
}
ImeState.OPTION -> {
lifecycleScope.launch {
if (keyboardTop == 0) {
viewDataBinding.etMessage.requestFocus() //이건 하위버전 대응
showKeyboard()
viewDataBinding.root.awaitLayoutChanged()
}
if (isVisibleKeyboard) {
hideKeyboard()
viewDataBinding.root.awaitLayoutChanged()
}
viewDataBinding.ibPlus.setImageDrawable(closeImage)
viewDataBinding.extra.isVisible = true
currentImeState = ImeState.OPTION
}
}
}
}
이와 같은 형태로 변경 ( 키보드 상태 변경할때까지 기다리고 다음 동작 한다 )
'Android' 카테고리의 다른 글
코루틴 좋아요 동기화 처리 (0) | 2022.11.07 |
---|---|
build.gradle에 정의한 manifestPlaceholders 값 주입 받기 (0) | 2022.11.05 |
Gson으로 sealed class 로 맵핑하기 (0) | 2022.10.04 |
특정 뷰위에 BottomSheet 올리기 (0) | 2022.09.29 |
카카오톡 잠금화면 구현 (0) | 2022.09.29 |