Compose 1.7のTextFieldはPOBox Plusで日本語変換できない
はじめに
(2024/10/24追記) 本件の修正がComposeのmainブランチにマージされました!近いうちにstableにも反映される予定です。 https://issuetracker.google.com/issues/373743376#comment5
タイトルの通り、Compose 1.7のTextFieldはPOBox Plusで日本語変換できません。
ひらがなを入力した段階で、入力が確定してしまい漢字などの変換ができません。
GoogleのIssueTrackerには登録済みです。
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に登録されている既知の問題です。
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で利用します。
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
EditText化に関連する問題で、Android 8.0ではViewを含んでいる画面でFocusManager.clearFocus()すると最初のViewにsetFocusされる問題があります。
本件の修正がmainブランチにマージされた!