PAYATHON 2026

MAUI 10 在 Pixel Watch 上为何收不到 Wear OS 实体按钮事件?

数据阿明

结论

应先确认设备是否将按钮事件分发给普通应用。按下按钮只会打开 Recent apps,而 MAUI 的 DispatchKeyEvent 没有日志,这更像是按钮由系统处理。仅修改 Stem1 判断或增加 return true,无法让未到达应用的事件出现。

原生 Kotlin 测试也失败了,进一步支持这个排查方向。不过,现有材料不足以确定 Pixel Watch 4 在该系统版本下的具体按键映射,也不能仅凭 WearableButtons 返回空列表断言设备完全不支持应用按键。

实体按钮不一定是应用可用的多功能按钮

Wear OS 官方文档介绍的多功能按钮,是设备向前台应用开放的按钮,映射为 KEYCODE_STEM_1、KEYCODE_STEM_2、KEYCODE_STEM_3。设备可以没有这类按钮,因此不能根据实体按钮的外观数量推断有多少个可供应用使用。参见 Wear OS 实体按钮文档。

KEYCODE_STEM_PRIMARY 表示手表的主电源/重置按钮,与上述多功能按钮不同。把它加入监听列表,并不代表系统会把对应事件交给应用。参见 KeyEvent 常量说明。

问题中提到按下按钮后会打开 Recent apps,应优先怀疑该操作被系统接管。这只是基于现象的判断,仍需结合事件日志确认。

先用日志区分事件缺失与界面未显示

现有 Kotlin 测试只在界面上显示 getButtonInfo() 返回非空的按钮。即使按键事件到达了 onKeyDown(),如果该键没有对应的按钮卡片,界面也无法显示它的按下状态。

建议在 Kotlin 测试 Activity 中增加以下诊断代码,直接观察所有传入事件,并在现有 onCreate() 中调用 logButtonMetadata():

override fun dispatchKeyEvent(event: KeyEvent): Boolean {
    android.util.Log.d(
        "ButtonDebug",
        "action=${event.action}, keyCode=${event.keyCode}, " +
            "repeat=${event.repeatCount}"
    )
    return super.dispatchKeyEvent(event)
}

private fun logButtonMetadata() {
    android.util.Log.d(
        "ButtonDebug",
        "buttonCount=${WearableButtons.getButtonCount(this)}"
    )

    listOf(
        KeyEvent.KEYCODE_STEM_1,
        KeyEvent.KEYCODE_STEM_2,
        KeyEvent.KEYCODE_STEM_3
    ).forEach { code ->
        val info = WearableButtons.getButtonInfo(this, code)
        android.util.Log.d(
            "ButtonDebug",
            "keyCode=$code, hasInfo=${info != null}"
        )
    }
}

官方说明,getButtonCount() 的计数包含电源按钮,返回值大于 1 时表示存在多功能按钮。getButtonInfo() 用于查询按钮元数据,不负责开启事件监听。参见 按钮元数据说明。

测试时,让应用位于前台、屏幕处于交互状态,并先确认 Activity 启动日志能正常显示,以排除 Logcat 过滤条件设置不当或调试进程选错的问题。

  • 如果两种应用的 DispatchKeyEvent/dispatchKeyEvent 都没有对应日志,而系统照常打开 Recent apps,应继续检查设备的系统按键策略。业务代码和 ViewModel 尚未参与这次按键处理。
  • 如果有事件日志,应以实际收到的 KeyCode 为依据,再检查它是否进入处理分支。
  • 如果事件到达但元数据为空,应分别处理这两个结果,避免用按钮卡片是否出现代替事件日志。

收到事件后,再调整 MAUI 的处理方式

原 C# 代码还有一个独立问题:它在 DispatchKeyEvent 中消费了 Down 事件,然后期待 OnKeyLongPress 被调用。这样会绕过后续默认分发,不能依赖普通回调链自动完成长按识别。

Android 的标准做法是在 OnKeyDown 中对首次按下调用 StartTracking() 并返回 true,再通过 OnKeyLongPress 和 OnKeyUp 处理长按及抬起。参见 KeyEvent.Callback 文档。

可以保留 DispatchKeyEvent 作为日志入口,但让它始终调用基类。将业务处理移入下面这些方法,替换原有处理逻辑:

private static bool IsStemButton(Keycode keyCode) =>
    keyCode == Keycode.Stem1 ||
    keyCode == Keycode.Stem2 ||
    keyCode == Keycode.Stem3;

public override bool OnKeyDown(Keycode keyCode, KeyEvent? e)
{
    if (!IsStemButton(keyCode) || e is null)
        return base.OnKeyDown(keyCode, e);

    if (e.RepeatCount == 0)
        e.StartTracking();

    return true;
}

public override bool OnKeyLongPress(Keycode keyCode, KeyEvent? e)
{
    if (!IsStemButton(keyCode))
        return base.OnKeyLongPress(keyCode, e);

    var viewModel = IPlatformApplication.Current?.Services
        .GetService<MainViewModel>();
    viewModel?.ResetTimer();
    return true;
}

public override bool OnKeyUp(Keycode keyCode, KeyEvent? e)
{
    if (!IsStemButton(keyCode) || e is null)
        return base.OnKeyUp(keyCode, e);

    if (e.IsTracking && !e.IsCanceled)
    {
        var viewModel = IPlatformApplication.Current?.Services
            .GetService<MainViewModel>();

        if (keyCode == Keycode.Stem1)
            viewModel?.ToggleTimer();
        else
            viewModel?.ResetTimer();
    }

    return true;
}

这段代码适用于系统已经向当前 Activity 分发相应 Stem 事件的情况。它调整了短按和长按的处理流程,无法改变设备的系统按键策略。

另外,GetService<MainViewModel>() 返回空值时,空条件调用会静默跳过操作。确认收到事件后,还要检查服务注册,以及解析出的 ViewModel 是否就是页面正在使用的实例。

SDK 信息与可用替代方案

“Android SDK:21.0”需要进一步说明是哪个工具或配置的版本号。如果指 Android API level 21,需要注意 KEYCODE_STEM_1 至 KEYCODE_STEM_3 是 API level 24 才加入的常量,但不能据此认定它就是当前事件缺失的原因。应分别核对项目编译目标、最低支持版本和设备实际 API level。参见 KeyEvent API 参考。

如果最终确认该设备没有向普通应用开放所需按钮,现有资料没有提供一种可通过普通 Activity 回调强制接管 Recent apps 按钮的方法。计时器应保留屏幕上的开始、暂停和重置操作;需要实体按钮时,可以在明确提供可用多功能按钮的设备或相应模拟环境上验证处理逻辑。

备注:内容仅供参考。