💭

Compose 1.7のTextFieldはPOBox Plusで日本語変換できない

2024/10/16に公開
2

はじめに

(2024/10/24追記) 本件の修正がComposeのmainブランチにマージされました!近いうちにstableにも反映される予定です。 https://issuetracker.google.com/issues/373743376#comment5

タイトルの通り、Compose 1.7のTextFieldはPOBox Plusで日本語変換できません。
ひらがなを入力した段階で、入力が確定してしまい漢字などの変換ができません。

GoogleのIssueTrackerには登録済みです。
https://issuetracker.google.com/issues/373743376

POBox Plusとは?

SonyのXperiaに標準搭載されていた、日本語入力アプリです。
ソニーストア:POBox Plusとは?

つまり、GboardやATOKなどと同じ日本語IMEです。
ただし、Google Playでは公開されておらず、通常はPOBox PlusがプリインストールされたXperiaを入手しないと利用できません。

現象が起きる対象端末は?

POBox Plus搭載端末の全てで起きるかは不明です。
ただし、下記の端末で現象の発生を確認できました。

  • Xperia 1 Android 11
  • Xperia XZ3 Android 10
  • Xperia ZL2 SOL25 Android 5.0

この中で最も発売日が新しいのは2019年発売のXperia 1です。
ただし、一部のXperiaユーザーは慣れ親しんだPOBox Plusを新しいXperiaに手動でインストールしてご利用されているケースもあるようです。

現象の回避方法について

一番簡単かつ確実なのは、Compose 1.6に戻す事です。

他の方法としてはTextFieldをAndroid ViewのEditTextに置き換えという手がありますが、困ったことにCompose 1.7にはCompose内のEditTextをタッチするとANRが発生する不具合があります。

EditTextをタッチするとANRが発生する不具合について

GoogleのIssueTrackerに登録されている既知の問題です。
https://issuetracker.google.com/issues/369354336

ANRが発生するかどうかはCompose側のレイアウトに依存します。
なお、私が開発に携わっているアプリではEditTextを使用しているほぼ全画面でANRが100%再現します。
すでに上記のIssueTrackerで100%ANR再現するアプリを共有済みです。

なぜANRが発生するのか

EditTextをタッチしてEditTextにフォーカスしたあと、なぜかInputMethodManager経由で間接的に AndroidComposeView#focusSearch(focused: View?, direction: Int): View? が呼ばれます。
しかし、既にAndroidComposeViewで表示しているEditTextにフォーカス済みなので、 AndroidComposeView#focusSearch では次にフォーカスするべき箇所を見つけられず、現在表示している全Composableを走査します(LazyListの中も全部走査している様子)。

この全Composableの走査がANRを引き起こします。

下記が、EditTextへのフォーカスで AndroidComposeView#focusSearch が呼ばれた際のスタックトレースです。

focusSearch:855, AndroidComposeView (androidx.compose.ui.platform)
focusSearch:1080, ViewGroup (android.view)
focusSearch:1080, ViewGroup (android.view)
focusSearch:1080, ViewGroup (android.view)
focusSearch:56, WorkaroundFocusSearchLayout (com.google.samples.apps.nowinandroid.util)
focusSearch:13240, View (android.view)
hasEditorInFocusSearchDirection:8981, TextView (android.widget)
onCreateInputConnection:9001, TextView (android.widget)
startInputInner:2306, InputMethodManager (android.view.inputmethod)
startInput:665, InputMethodManager$DelegateImpl (android.view.inputmethod)
checkFocus:167, ImeFocusController (android.view)
checkFocus:2505, InputMethodManager (android.view.inputmethod)
viewClicked:2704, InputMethodManager (android.view.inputmethod)
viewClicked:13640, TextView (android.widget)
onTouchEvent:11479, TextView (android.widget)
dispatchTouchEvent:15004, View (android.view)
dispatchTransformedTouchEvent:3121, ViewGroup (android.view)
dispatchTouchEvent:2802, ViewGroup (android.view)
dispatchTransformedTouchEvent:3121, ViewGroup (android.view)
dispatchTouchEvent:2802, ViewGroup (android.view)
invoke:113, PointerInteropFilter_androidKt$pointerInteropFilter$3 (androidx.compose.ui.input.pointer)
invoke:105, PointerInteropFilter_androidKt$pointerInteropFilter$3 (androidx.compose.ui.input.pointer)
invoke:309, PointerInteropFilter$pointerInputFilter$1$dispatchToView$3 (androidx.compose.ui.input.pointer)
invoke:294, PointerInteropFilter$pointerInputFilter$1$dispatchToView$3 (androidx.compose.ui.input.pointer)
lambda 'apply' in 'toMotionEventScope':81, PointerInteropUtils_androidKt (androidx.compose.ui.input.pointer)
toMotionEventScope-ubNVwUQ:73, PointerInteropUtils_androidKt (androidx.compose.ui.input.pointer)
toMotionEventScope-d-4ec7I:35, PointerInteropUtils_androidKt (androidx.compose.ui.input.pointer)
dispatchToView:294, PointerInteropFilter$pointerInputFilter$1 (androidx.compose.ui.input.pointer)
onPointerEvent-H0pRuoY:229, PointerInteropFilter$pointerInputFilter$1 (androidx.compose.ui.input.pointer)
lambda 'with' in 'onPointerEvent':366, BackwardsCompatNode (androidx.compose.ui.node)
onPointerEvent-H0pRuoY:365, BackwardsCompatNode (androidx.compose.ui.node)
lambda 'dispatchForKind-6rFNWt0' in 'dispatchMainEventPass':367, Node (androidx.compose.ui.input.pointer)
dispatchForKind-6rFNWt0:436, Node (androidx.compose.ui.input.pointer)
lambda 'dispatchIfNeeded' in 'dispatchMainEventPass':366, Node (androidx.compose.ui.input.pointer)
dispatchIfNeeded:591, Node (androidx.compose.ui.input.pointer)
dispatchMainEventPass:361, Node (androidx.compose.ui.input.pointer)
lambda 'forEach' in 'dispatchMainEventPass':373, Node (androidx.compose.ui.input.pointer)
forEach:466, Node (androidx.compose.ui.input.pointer)
lambda 'dispatchIfNeeded' in 'dispatchMainEventPass':372, Node (androidx.compose.ui.input.pointer)
dispatchIfNeeded:591, Node (androidx.compose.ui.input.pointer)
dispatchMainEventPass:361, Node (androidx.compose.ui.input.pointer)
lambda 'forEach' in 'dispatchMainEventPass':229, NodeParent (androidx.compose.ui.input.pointer)
forEach:466, NodeParent (androidx.compose.ui.input.pointer)
dispatchMainEventPass:228, NodeParent (androidx.compose.ui.input.pointer)
dispatchChanges:144, HitPathTracker (androidx.compose.ui.input.pointer)
process-BIzXfog:120, PointerInputEventProcessor (androidx.compose.ui.input.pointer)
sendMotionEvent-8iAsVTc:1994, AndroidComposeView (androidx.compose.ui.platform)
lambda 'trace' in 'handleMotionEvent':1945, AndroidComposeView (androidx.compose.ui.platform)
trace:28, AndroidComposeView (androidx.compose.ui.platform)
handleMotionEvent-8iAsVTc:1856, AndroidComposeView (androidx.compose.ui.platform)
dispatchTouchEvent:1829, AndroidComposeView (androidx.compose.ui.platform)
dispatchTransformedTouchEvent:3121, ViewGroup (android.view)
dispatchTouchEvent:2802, ViewGroup (android.view)
dispatchTransformedTouchEvent:3121, ViewGroup (android.view)
dispatchTouchEvent:2802, ViewGroup (android.view)
dispatchTransformedTouchEvent:3121, ViewGroup (android.view)
dispatchTouchEvent:2802, ViewGroup (android.view)
dispatchTransformedTouchEvent:3121, ViewGroup (android.view)
dispatchTouchEvent:2802, ViewGroup (android.view)
superDispatchTouchEvent:498, DecorView (com.android.internal.policy)
superDispatchTouchEvent:1899, PhoneWindow (com.android.internal.policy)
dispatchTouchEvent:4262, Activity (android.app)
dispatchTouchEvent:456, DecorView (com.android.internal.policy)
dispatchPointerEvent:15263, View (android.view)
processPointerEvent:6548, ViewRootImpl$ViewPostImeInputStage (android.view)
onProcess:6348, ViewRootImpl$ViewPostImeInputStage (android.view)
deliver:5804, ViewRootImpl$InputStage (android.view)
onDeliverToNext:5861, ViewRootImpl$InputStage (android.view)
forward:5827, ViewRootImpl$InputStage (android.view)
forward:5992, ViewRootImpl$AsyncInputStage (android.view)
apply:5835, ViewRootImpl$InputStage (android.view)
apply:6049, ViewRootImpl$AsyncInputStage (android.view)
deliver:5808, ViewRootImpl$InputStage (android.view)
onDeliverToNext:5861, ViewRootImpl$InputStage (android.view)
forward:5827, ViewRootImpl$InputStage (android.view)
apply:5835, ViewRootImpl$InputStage (android.view)
deliver:5808, ViewRootImpl$InputStage (android.view)
deliverInputEvent:8857, ViewRootImpl (android.view)
doProcessInputEvents:8808, ViewRootImpl (android.view)
enqueueInputEvent:8777, ViewRootImpl (android.view)
onInputEvent:8980, ViewRootImpl$WindowInputEventReceiver (android.view)
dispatchInputEvent:267, InputEventReceiver (android.view)
nativePollOnce:-1, MessageQueue (android.os)
next:335, MessageQueue (android.os)
loopOnce:161, Looper (android.os)
loop:288, Looper (android.os)
main:7898, ActivityThread (android.app)
invoke:-1, Method (java.lang.reflect)
run:548, RuntimeInit$MethodAndArgsCaller (com.android.internal.os)
main:936, ZygoteInit (com.android.internal.os)

Compose 1.7のまま、EditTextフォーカス時のANRを回避するには?

色々と試して見たのですが、EditTextフォーカス時に AndroidComposeView#focusSearch(focused: View?, direction: Int): View? が呼び出されるのをブロックする方法で回避できました。

具体的には下記のWorkaroundFocusSearchLayoutを追加し、EditTextは必ずWorkaroundFocusSearchLayoutの子Viewとして配置してからComposeで利用します。

WorkaroundFocusSearchLayout.kt
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.FrameLayout

/**
 * A workaround for the ANR issue on Compose 1.7.x.
 * https://issuetracker.google.com/issues/369354336
 *
 * Only one View must be placed directly under this layout.
 * Placing multiple Views may cause problems with focus movement.
 *
 * Focusing on EditText in Compose will cause ANR depending on the layout of Compose side.
 *
 * This is because AndroidComposeView's focusSearch (focused: View?, direction: Int) is executed
 * after EditText is focused.
 * If AndroidComposeView#focusSearch is executed while the focus is already on EditText,
 * it will cause ANR because ComposeView searches all Composable.
 *
 * To prevent this problem, do not call super.focusSearch in the WorkaroundFocusSearchLayout#focusSearch
 * and return immediately.
 *
 * This prevents AndroidComposeView#focusSearch from being executed.
 */
class WorkaroundFocusSearchLayout : FrameLayout {

    constructor(
        context: Context,
    ) : super(context)

    constructor(
        context: Context,
        attrs: AttributeSet?,
    ) : super(context, attrs)

    constructor(
        context: Context,
        attrs: AttributeSet?,
        defStyleAttr: Int,
    ) : super(context, attrs, defStyleAttr)

    constructor(
        context: Context,
        attrs: AttributeSet?,
        defStyleAttr: Int,
        defStyleRes: Int,
    ) : super(context, attrs, defStyleAttr, defStyleRes)

    override fun focusSearch(focused: View?, direction: Int): View? {
        return null
    }
}

例えばMaterialのTextInputLayoutとTextInputEditTextを使う場合、レイアウトXMLは下記の様になります。

<?xml version="1.0" encoding="utf-8"?>
<com.example.WorkaroundFocusSearchLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <com.google.android.material.textfield.TextInputLayout
        android:id="@+id/inputLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <com.google.android.material.textfield.TextInputEditText
            android:id="@+id/editText"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />

    </com.google.android.material.textfield.TextInputLayout>

</com.example.WorkaroundFocusSearchLayout>

WorkaroundFocusSearchLayout#focusSearch で常にnullを返すとフォーカス周りに問題が発生しそうですが、軽く動かした限りは問題なさそうです。
恐らく、WorkaroundFocusSearchLayout内に複数の子Viewが存在しなければ大丈夫でしょう、たぶん。

おわりに

特定端末で日本語変換できない問題だけでは無く、(Composeレイアウトに依存するとはいえ)100%ANRを再現できる不具合を引き当ててしまい大変辛いです!!

Discussion