일반적으로 앱의 입력창에 무언가 타이핑 한 후에 키보드가 아닌 곳을 터치하여 키보드를 숨기는 UX가 보편화 되어있다. 물론 안드로이드 유저의 특권인 Back 키나 Back 제스처를 사용해도 되지만, 누구나 실수로 백키를 연타하여 작성 중이던 데이터를 날려버린 경험이 있을 것이다.

우리 “친절한” 안드로이드 개발자들은 키보드 밖을 터치했을 때 키보드가 닫히도록 배려를 해 주자. (내가 사용하던) 기존 View의 EditText의 포커스를 해제하던 방법과 Jetpack Compose TextField 에서 처리하는 방법이 조금 달라서 정리 차원해서 정리해보았다.


1. 터치 이벤트를 감지하고 EditText 포커싱 해제

기존 안드로이드 View에서 EditText에 텍스트를 입력한 후 키보드를 숨겨야 할 때에는 액티비티에서 터치 이벤트를 감지하여 EditText 일 때에는 도로 포커스를 해제하는 방법을 사용했다.

//MainActivity.kt
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
    if (currentFocus is EditText) {
        val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
        inputMethodManager.hideSoftInputFromWindow(currentFocus!!.windowToken, 0)
        currentFocus!!.clearFocus()
    }

    return try {
        super.dispatchTouchEvent(ev)
    } catch (e: Exception) {
        true
    }
}

포커스가 EditText 일 때 InputMethodManager를 통해 키보드(Soft Input)을 숨겨주도록 하여 사용했다.


2. TextField 포커싱 여부 체크

Jetpack Compose는 선언형 UI이기 때문에 현재의 뷰가 EditText인지 비교하는 위의 코드를 쓰기가 난해했다. 그래서 TextField의 상태 값에 따라 바뀌는 Boolean 변수를 하나 생성했다.

class MainActivity : ComponentActivity() {
    var isTextFieldFocused = false

    ...

    override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
        if (isTextFieldFocused) {
            val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
            inputMethodManager.hideSoftInputFromWindow(currentFocus!!.windowToken, 0)
            currentFocus!!.clearFocus()
        }

        return try {
            super.dispatchTouchEvent(ev)
        } catch (e: Exception) {
            true
        }
    }

    @Composable
    fun MainContent() {
        val focusRequester by remember { mutableStateOf(FocusRequester()) }
        var text by remember { mutableStateOf("") }

        TextField(
            value = text,
            onValueChange = {
                text = it
            },
            modifier = Modifier
                .focusRequester(focusRequester = focusRequester)
                .onFocusChanged {
                    isTextFieldFocused = it.isFocused
                }
        )
    }
}


나는 위 방법으로 포커스를 클리어 하는 방법을 찾았지만, 동료가 보더니 이 방법을 쓰면 TextField 가 늘어날 때마다 변수를 새로 생성해 주어야 하는 단점이 있다는 것을 알려주었다. 어떻게 하지 고민을 한참 했는데, 동료가 알려준 방법은 의외로 간단했다.


3. 전체화면의 터치 이벤트 감지

TextField가 아닌, 그것을 감싸고 있는 전체 화면의 터치 이벤트를 캐치해서 포커스를 해제하는 영리한 방법을 동료가 알려주었다. 함수명을 보니 detectTapGestures로 태핑을 감지한다는 뜻이 더 맞는 것 같다.

컴포저블을 깔끔하게 쓰기 위해 Modifier의 Extension 함수를 추가해주었다.

fun Modifier.addFocusCleaner(focusManager: FocusManager, doOnClear: () -> Unit = {}): Modifier {
    return this.pointerInput(Unit) {
        detectTapGestures(onTap = {
            doOnClear()
            focusManager.clearFocus()
        })
    }
}


그리고 TextField를 Column으로 감싸주고, Column을 터치할 때 위에 만들어준 함수로 포커스를 해제하도록 만들었다.

@Composable
fun MainContent() {
    val focusRequester by remember { mutableStateOf(FocusRequester()) }
    val focusManager = LocalFocusManager.current
    var text by remember { mutableStateOf("안뇽") }

    Column(modifier = Modifier
        .fillMaxSize()
        .addFocusCleaner(focusManager)
    ) {
        TextField(
            value = text,
            onValueChange = {
                text = it
            },
            modifier = Modifier
                .focusRequester(focusRequester = focusRequester)
                .onFocusChanged {
                    isTextFieldFocused = it.isFocused
                }
        )
    }
}



이런 발상으로 생각하지 못한 바보 같은 나를 탓하며 포스트 마무리는

맥주 마시고 싶당 ㅎㅎ