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. 这几个坑要提前说
- 触摸层不要做成全屏透明 View。全屏层会吞掉前台应用的点击、滑动和输入;这里只放左右各 24dp 的窄条。
- 判断方向要用
event.rawX,不要用event.x。右侧窗口里的event.x是相对于右侧小窗口的局部坐标。 - 失败手势也会被边缘层消费,所以边缘区域越窄越好;实际项目需要结合设备壳、返回手势习惯调整
24dp和120dp两个阈值。 - 无障碍服务被关闭后,左右触摸层会一起消失;应用内要给出明确的开启引导和当前服务状态。
- 这是开发版/内部分发方案。对外发布时要确认无障碍 API 符合产品用途、用户知情和应用商店政策;不要用无障碍服务绕过用户授权。
- 某些安全页面、系统确认页或厂商定制页面可能限制覆盖层行为,不能承诺所有系统界面都与普通应用完全一致。
Android 9 没有可直接打开的系统侧滑返回开关。开发版如果必须补齐这个体验,最小可行方案是:
text
无障碍服务负责"站到系统层"
无障碍覆盖层负责"接住边缘触摸"
GLOBAL_ACTION_BACK 负责"执行全局返回"
它不是把 Android 9 改造成 Android 10,而是在应用权限允许的范围内,补出一个可用的系统级返回入口。