7
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

android:enableOnBackInvokedCallback="true"にしたときのバックキーのKeyEventの変化

Posted at

予測型「戻る」ジェスチャーをオプトインしていますか?

オプトインした場合のDialogでのバックキー検出の問題に遭遇しました。

予測型「戻る」ジェスチャーをオプトインするには、 AndroidManifest.xml<application> タグ内で、 android:enableOnBackInvokedCallback フラグを true に設定します。

AndroidManifest.xml
<application
    ...
    android:enableOnBackInvokedCallback="true"
    ... >
...
</application>

AppCompatActivityでの変化

このとき、

注: OnBackPressedCallback は、android:enableOnBackInvokedCallback の値に関係なく常に呼び出されます。つまり、システム アニメーションを無効にしても、OnBackPressedCallback を使用している場合、アプリの戻る処理のロジックには影響しません。

とある通り、ComponentActivityOnBackPressedCallback は、android:enableOnBackInvokedCallbackの値に関係なく呼び出されます。
一方、android:enableOnBackInvokedCallback="true" にした場合、ActivityonBackPressed() もコールされなくなりますし、onKeyUp() などもバックキーのイベントが通知されなくなります。

MainActivity.kt
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以上では、 onBackInvokedDispatcherOnBackInvokedCallback を登録することができます。
この動作が、 android:enableOnBackInvokedCallback の値によって排他的な動作になっているので注意が必要です。
android:enableOnBackInvokedCallback="false" の場合、 OnKeyListener がコールされますが、OnBackInvokedCallback がコールされません。
OnBackInvokedCallback で検出したい場合、 android:enableOnBackInvokedCallback="true" にする必要があります。こうすると OnKeyListener がコールされなくなります。

MyDialogFragment.kt
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 でイベントが排他的に振り分けられているようです。

ViewRootImpl.java
@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 は呼ばれなくなる。
ということでした。まあ、書き下してみればおかしな挙動というわけでもない気がしますが、前提知識が無いと混乱しそうなのでご注意を。

7
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?