予測型「戻る」ジェスチャーをオプトインしていますか?
オプトインした場合のDialogでのバックキー検出の問題に遭遇しました。
予測型「戻る」ジェスチャーをオプトインするには、 AndroidManifest.xml
の <application>
タグ内で、 android:enableOnBackInvokedCallback
フラグを true
に設定します。
<application
...
android:enableOnBackInvokedCallback="true"
... >
...
</application>
AppCompatActivityでの変化
このとき、
注: OnBackPressedCallback は、android:enableOnBackInvokedCallback の値に関係なく常に呼び出されます。つまり、システム アニメーションを無効にしても、OnBackPressedCallback を使用している場合、アプリの戻る処理のロジックには影響しません。
とある通り、ComponentActivity
の OnBackPressedCallback
は、android:enableOnBackInvokedCallback
の値に関係なく呼び出されます。
一方、android:enableOnBackInvokedCallback="true"
にした場合、Activity
の onBackPressed()
もコールされなくなりますし、onKeyUp()
などもバックキーのイベントが通知されなくなります。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
...
onBackPressedDispatcher.addCallback(object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
// android:enableOnBackInvokedCallback の値にかかわらずコールされる
}
})
}
override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
// android:enableOnBackInvokedCallback="true"ならバックキーでコールされない
return super.onKeyUp(keyCode, event)
}
override fun onBackPressed() {
// android:enableOnBackInvokedCallback="true" ならコールされない
super.onBackPressed()
}
バックキーのハンドリングは OnBackPressedCallback
で行うようにしておけば問題無いでしょう。
Dialogでの変化
一方、Dialogの場合、ちょっと事情が異なります。
DialogFragmentではDialogに対して、OnKeyListenerを登録することでバックキーのイベントを拾うことができます。
また、API 33以上では、 onBackInvokedDispatcher
に OnBackInvokedCallback
を登録することができます。
この動作が、 android:enableOnBackInvokedCallback
の値によって排他的な動作になっているので注意が必要です。
android:enableOnBackInvokedCallback="false"
の場合、 OnKeyListener
がコールされますが、OnBackInvokedCallback
がコールされません。
OnBackInvokedCallback
で検出したい場合、 android:enableOnBackInvokedCallback="true"
にする必要があります。こうすると OnKeyListener
がコールされなくなります。
override fun onStart() {
super.onStart()
requireDialog().setOnKeyListener { dialog, keyCode, event ->
if (keyCode == KeyEvent.KEYCODE_BACK && event.action == KeyEvent.ACTION_UP) {
// android:enableOnBackInvokedCallback="true" ならバックキーでコールされない
}
false
}
if (VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) {
requireDialog().onBackInvokedDispatcher.registerOnBackInvokedCallback(OnBackInvokedDispatcher.PRIORITY_DEFAULT) {
// android:enableOnBackInvokedCallback="false" ならコールされない
}
}
}
どうやら ViewRootImpl
でイベントが排他的に振り分けられているようです。
@Override
protected int onProcess(QueuedInputEvent q) {
if (q.mEvent instanceof KeyEvent) {
final KeyEvent event = (KeyEvent) q.mEvent;
// If the new back dispatch is enabled, intercept KEYCODE_BACK before it reaches the
// view tree or IME, and invoke the appropriate {@link OnBackInvokedCallback}.
if (isBack(event)
&& mContext != null
&& WindowOnBackInvokedDispatcher.isOnBackInvokedCallbackEnabled(mContext)) {
OnBackInvokedCallback topCallback =
getOnBackInvokedDispatcher().getTopCallback();
if (event.getAction() == KeyEvent.ACTION_UP) {
if (topCallback != null) {
topCallback.onBackInvoked();
return FINISH_HANDLED;
}
} else {
// Drop other actions such as {@link KeyEvent.ACTION_DOWN}.
return FINISH_NOT_HANDLED;
}
}
}
if (mInputQueue != null && q.mEvent instanceof KeyEvent) {
mInputQueue.sendInputEvent(q.mEvent, q, true, this);
return DEFER;
}
return FORWARD;
}
android:enableOnBackInvokedCallback="true"
の場合 WindowOnBackInvokedDispatcher.isOnBackInvokedCallbackEnabled(mContext)
がtrueを返すので、onBackInvoked()
がコールされて、FINISH_HANDLED
が返却され、イベントがここで消費されてしまうので、onKeyListenerがコールされなくなるようです。
まとめ
ようするに、
OnBackInvokedCallback
でバックキーイベントを拾うには、 android:enableOnBackInvokedCallback="true"
である必要があり、この場合、OnKeyListner
は呼ばれなくなる。
OnKeyListner
でバックキーイベントを拾うには、android:enableOnBackInvokedCallback="false"
である必要があり、この場合、OnBackInvokedCallback
は呼ばれなくなる。
ということでした。まあ、書き下してみればおかしな挙動というわけでもない気がしますが、前提知識が無いと混乱しそうなのでご注意を。