Android 9 开发板实现系统侧滑返回

Android 9 开发板实现系统侧滑返回

Android 9(API 28)的 AOSP 没有 Android 10 那种统一的"从屏幕左侧/右侧边缘滑动返回"系统手势。Android 10 才开始提供全设备的手势导航,边缘内滑会被系统解释成 Back;某些 Android 9 厂商 ROM 虽然有自己的侧滑手势,但不能当成通用能力。

普通应用也不能直接调用隐藏的 InputManager 注入 Back,或者执行 input keyevent 4 来伪造系统按键:这类方式依赖隐藏 API、shell/root 权限,开发版能临时验证,不能作为正常应用方案。

最后采用的链路是:

text 复制代码
AccessibilityService
        ↓
TYPE_ACCESSIBILITY_OVERLAY:在屏幕左右各放一个窄触摸层
        ↓
判断边缘水平滑动
        ↓
performGlobalAction(GLOBAL_ACTION_BACK)

这个方案对用户表现为系统级返回:当前前台应用是谁,返回动作就作用于谁。

1. 注册无障碍服务

AndroidManifest.xml 只保留必要配置:

xml 复制代码
<service
    android:name=".EdgeBackAccessibilityService"
    android:exported="true"
    android:label="开发版侧滑返回"
    android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">

    <intent-filter>
        <action android:name="android.accessibilityservice.AccessibilityService" />
    </intent-filter>

    <meta-data
        android:name="android.accessibilityservice"
        android:resource="@xml/edge_back_accessibility_service" />
</service>

res/xml/edge_back_accessibility_service.xml

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:accessibilityEventTypes="typeWindowStateChanged"
    android:accessibilityFeedbackType="feedbackGeneric"
    android:canRetrieveWindowContent="false"
    android:notificationTimeout="100" />

首次使用必须由用户在"设置 → 无障碍"中手动打开服务。普通应用不能静默开启无障碍服务。

2. 核心实现

下面是 Kotlin 核心代码。Android 组件入口本身必须继承 AccessibilityService;除此之外没有再封装手势识别类、悬浮窗管理类或辅助函数,逻辑全部放在回调里。

kotlin 复制代码
import android.accessibilityservice.AccessibilityService
import android.graphics.Color
import android.graphics.PixelFormat
import android.os.SystemClock
import android.view.Gravity
import android.view.MotionEvent
import android.view.View
import android.view.WindowManager
import kotlin.math.abs

class EdgeBackAccessibilityService : AccessibilityService() {

    private lateinit var windowManager: WindowManager
    private lateinit var leftEdge: View
    private lateinit var rightEdge: View

    override fun onServiceConnected() {
        super.onServiceConnected()

        windowManager = getSystemService(WINDOW_SERVICE) as WindowManager

        val density = resources.displayMetrics.density
        val edgeWidth = (24 * density).toInt()

        var downX = 0f
        var downY = 0f
        var downAt = 0L

        val edgeTouchListener = View.OnTouchListener { _, event ->
            when (event.actionMasked) {
                MotionEvent.ACTION_DOWN -> {
                    downX = event.rawX
                    downY = event.rawY
                    downAt = SystemClock.uptimeMillis()
                }

                MotionEvent.ACTION_UP -> {
                    val dx = event.rawX - downX
                    val dy = event.rawY - downY
                    val duration = SystemClock.uptimeMillis() - downAt
                    val screenWidth = resources.displayMetrics.widthPixels.toFloat()

                    val fromLeft = downX < screenWidth / 2f
                    val horizontalEnough = abs(dx) > abs(dy) * 1.5f
                    val distanceEnough = abs(dx) > 120 * density
                    val durationEnough = duration < 800
                    val directionEnough =
                        (fromLeft && dx > 0) || (!fromLeft && dx < 0)

                    if (horizontalEnough &&
                        distanceEnough &&
                        durationEnough &&
                        directionEnough
                    ) {
                        performGlobalAction(GLOBAL_ACTION_BACK)
                    }
                }
            }

            // 边缘层只占 24dp,触摸期间必须持续消费事件。
            true
        }

        leftEdge = View(this).apply {
            setBackgroundColor(Color.TRANSPARENT)
            setOnTouchListener(edgeTouchListener)
        }

        rightEdge = View(this).apply {
            setBackgroundColor(Color.TRANSPARENT)
            setOnTouchListener(edgeTouchListener)
        }

        val flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
            WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or
            WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN

        windowManager.addView(
            leftEdge,
            WindowManager.LayoutParams(
                edgeWidth,
                WindowManager.LayoutParams.MATCH_PARENT,
                WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY,
                flags,
                PixelFormat.TRANSLUCENT
            ).apply {
                gravity = Gravity.START
            }
        )

        windowManager.addView(
            rightEdge,
            WindowManager.LayoutParams(
                edgeWidth,
                WindowManager.LayoutParams.MATCH_PARENT,
                WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY,
                flags,
                PixelFormat.TRANSLUCENT
            ).apply {
                gravity = Gravity.END
            }
        )
    }

    override fun onDestroy() {
        if (::leftEdge.isInitialized) {
            windowManager.removeViewImmediate(leftEdge)
        }
        if (::rightEdge.isInitialized) {
            windowManager.removeViewImmediate(rightEdge)
        }
        super.onDestroy()
    }

    override fun onAccessibilityEvent(event: android.view.accessibility.AccessibilityEvent) {}

    override fun onInterrupt() {}
}

3. 为什么不是普通悬浮窗?

TYPE_APPLICATION_OVERLAY 需要用户授予"在其他应用上层显示"权限,而且它只是普通应用窗口;这里使用的是无障碍服务专用的 TYPE_ACCESSIBILITY_OVERLAY,由已启用的 AccessibilityService 创建,不需要额外申请 SYSTEM_ALERT_WINDOW

真正触发返回的也不是 KEYCODE_BACK 注入,而是公开的:

kotlin 复制代码
performGlobalAction(GLOBAL_ACTION_BACK)

官方定义中,这个全局动作不依赖当前位于哪个应用或哪个页面,正好满足"系统级返回"的要求。

4. 这几个坑要提前说

  1. 触摸层不要做成全屏透明 View。全屏层会吞掉前台应用的点击、滑动和输入;这里只放左右各 24dp 的窄条。
  2. 判断方向要用 event.rawX,不要用 event.x。右侧窗口里的 event.x 是相对于右侧小窗口的局部坐标。
  3. 失败手势也会被边缘层消费,所以边缘区域越窄越好;实际项目需要结合设备壳、返回手势习惯调整 24dp120dp 两个阈值。
  4. 无障碍服务被关闭后,左右触摸层会一起消失;应用内要给出明确的开启引导和当前服务状态。
  5. 这是开发版/内部分发方案。对外发布时要确认无障碍 API 符合产品用途、用户知情和应用商店政策;不要用无障碍服务绕过用户授权。
  6. 某些安全页面、系统确认页或厂商定制页面可能限制覆盖层行为,不能承诺所有系统界面都与普通应用完全一致。

Android 9 没有可直接打开的系统侧滑返回开关。开发版如果必须补齐这个体验,最小可行方案是:

text 复制代码
无障碍服务负责"站到系统层"
无障碍覆盖层负责"接住边缘触摸"
GLOBAL_ACTION_BACK 负责"执行全局返回"

它不是把 Android 9 改造成 Android 10,而是在应用权限允许的范围内,补出一个可用的系统级返回入口。

官方依据

相关推荐
2601_962293532 小时前
Appium+python自动化(十二)- Android UIAutomator终极定位凶器(超详解)
android·自动化测试·appium·定位·uiautomator
mmsx2 小时前
MapLibre 自定义比例尺:Web 墨卡托屏幕距离换算公式与实现
android·源码·地图·maplibre
可乐鸡翅yeah_2 小时前
HLS CORS 跨域问题完整排查实战,解决 M3U8 与 TS 分片跨域报错
android·ios·音视频·m3u8·音视频在线播放
邪修king3 小时前
Linux系统篇(二十三) 基础 IO:从“文件”到“文件描述符”,彻底理解重定向
android·java·linux
灵境(虚幻知音)3 小时前
效能评估指标体系构建:方法、流程与模板
android·数据库
光影少年4 小时前
React18 对RN 的影响
android·前端·react.js·ios·前端框架
杉氧4 小时前
丝滑的奥秘:Reanimated 3 动画引擎与手势处理(Gesture Handler)
android·前端·react native
我命由我123454 小时前
Compose Codelab 学习 - Jetpack Compose 中的状态
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
sbjdhjd4 小时前
在 8 字符、字母数字过滤与 PHP 版本差异下复盘临时文件命令执行链 | (8字符绕过)
android·java·开发语言·安全·web安全·数据挖掘·php