一、项目介绍
1. 背景与动机
在日常使用 Android 设备时,截屏是一项非常常见且实用的功能。系统自带的截屏方式多为:
-
同时按下 电源键 + 音量减键
-
在部分设备上,长按 电源键 弹出截屏选项
-
在部分厂商定制的系统中,还可使用 快捷手势(如手掌滑动)
但这些方式存在以下不足:
-
按键操作不够直观:对于手指不够灵活的用户,按键组合可能会误触或按不到。
-
手掌滑动受限:edge case 较多,不同机型灵敏度不一。
-
一致性差:不同厂商和系统版本截屏方法不统一,用户需不断适应。
为了提供更 自然、统一、可定制 的截屏交互体验,本项目提出:使用“三指并拢长按”或“三指向下滑动”触发截屏,让用户只需轻轻三指操作,即可完成截屏。该方案具有以下优势:
-
直观:三根手指并拢或滑动,符合用户直觉
-
跨机型:基于原生触摸事件与 AccessibilityService,无需系统定制
-
可扩展:可自由定义手势样式与触发条件
2. 项目目标
-
在 Android 5.0 及以上系统中,实现“三指截屏”功能
-
支持两种触发手势:
-
三指并拢长按(持续 500ms 以上)
-
三指向下快速滑动
-
-
使用 AccessibilityService 利用
performGlobalAction(GLOBAL_ACTION_TAKE_SCREENSHOT)
API 触发截屏,无需 ROOT -
提供完整示例工程,所有代码和布局整合,注释详尽
-
文章结构严格按照:
-
项目介绍
-
相关技术知识与解析
-
实现思路与整合代码
-
方法解读
-
项目总结
-
二、相关技术知识与解析
要完成“三指截屏”功能,需要掌握以下几个关键技术点:
-
多点触控(Multi-Touch)事件处理
-
自定义手势检测
-
AccessibilityService 服务与截屏 API
-
前台服务与权限申请
下面逐一进行详细解析。
1. 多点触控事件处理
Android 中所有触摸事件都由 MotionEvent
对象承载。通过 View
或 Activity
的 dispatchTouchEvent
、onTouchEvent
可接收触摸数据。多点触控时,MotionEvent
提供以下常用方法:
-
getPointerCount()
:当前屏幕上触摸点的数量 -
getActionMasked()
:触摸事件类型(兼容多点) -
getActionIndex()
:当前操作的指针索引 -
getX(int pointerIndex)
,getY(int pointerIndex)
:指定指针的坐标
常见触摸类型(getActionMasked()
返回值):
常量 | 含义 |
---|---|
ACTION_DOWN | 第一个手指按下 |
ACTION_POINTER_DOWN | 非第一个手指按下(多点按下时) |
ACTION_MOVE | 手指移动 |
ACTION_POINTER_UP | 非最后一个手指抬起 |
ACTION_UP | 最后一个手指抬起 |
ACTION_CANCEL | 事件中断 |
要检测“三指并拢”或“三指滑动”,需在 ACTION_POINTER_DOWN
阶段统计指针数量,并在 ACTION_MOVE
阶段分析手势轨迹及持续时间。
2. 自定义手势检测
Android 系统提供 GestureDetector
用于单点手势识别(如双击、长按、滑动)。但对多点手势并不直接支持,因此需手动实现。思路:
-
维护当前指针信息:在自定义管理类中保存所有指针的起始位置、当前坐标、按下时间等
-
定义阈值:
-
三指并拢最大间距:如三指间距小于 100px 认定为并拢
-
长按时长:500ms 以上
-
滑动最小距离:Y 轴下滑超过 200px
-
-
手势流程:
-
三指同时按下 → 记录起始时间与位置
-
在
ACTION_MOVE
中判断:-
若三指位置聚拢且手指保持静止超过阈值 → 触发长按截屏
-
若三指向下滑动且满足滑动距离 → 触发滑动截屏
-
-
3. AccessibilityService 与截屏 API
从 Android 7.0(API 24)开始,AccessibilityService
提供了系统级截屏接口:
// 在无障碍服务中调用
performGlobalAction(GLOBAL_ACTION_TAKE_SCREENSHOT);
使用步骤:
-
声明 AccessibilityService
-
在系统设置中开启服务
-
在服务中响应手势检测结果,调用
performGlobalAction
相比 MediaProjection API,AccessibilityService 截屏简单、无需用户二次授权,且支持保存至系统默认截屏目录。
4. 前台服务与权限申请
为了保证手势识别在应用切换、锁屏情况下依然可用,通常需要将手势检测放在一个 前台服务 中运行,并绑定 WindowManager 以截获全局触摸。关键点:
-
Android 8.0+ 对后台服务限制严格,必须调用
startForeground()
-
需要申请
SYSTEM_ALERT_WINDOW
(悬浮窗)权限,以便创建透明触摸层 -
AccessibilityService 需要用户手动在 “无障碍” 设置中开启
三、实现思路与整合代码
下面将所有 Java 代码和 XML 布局整合在一个文件中,使用注释分隔不同部分,配以极其详细的说明。请直接复制到项目中即可完整运行。
// =========================================================================
// 文件:ThreeFingerScreenshotApp.java
// 描述:将所有代码整合到一起,包含 Activity、Service、手势管理、布局资源
// =========================================================================
package com.example.threefingerscreenshot;
import android.accessibilityservice.AccessibilityService;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.graphics.PixelFormat;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.provider.Settings;
import android.util.Log;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityEvent;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.Toast;
/* --------------------------
部分一:MainActivity
负责启动并引导用户开启 AccessibilityService 与悬浮窗权限
-------------------------- */
public class MainActivity extends AppCompatActivity {
private static final int REQUEST_OVERLAY_PERMISSION = 1001;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 绑定布局
setContentView(R.layout.activity_main);
// 1. 检查并请求悬浮窗权限
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (!Settings.canDrawOverlays(this)) {
Intent intent = new Intent(
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
android.net.Uri.parse("package:" + getPackageName()));
startActivityForResult(intent, REQUEST_OVERLAY_PERMISSION);
Toast.makeText(this,
"请授权悬浮窗权限,以便实现全局手势监听", Toast.LENGTH_LONG).show();
}
}
// 2. 提示用户开启无障碍服务
Toast.makeText(this,
"请在“设置 → 无障碍”中启用 “三指截屏” 服务", Toast.LENGTH_LONG).show();
// 3. 启动手势监听前台服务
startService(new Intent(this, ThreeFingerService.class));
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_OVERLAY_PERMISSION) {
if (!Settings.canDrawOverlays(this)) {
Toast.makeText(this, "未获得悬浮窗权限,功能无法使用", Toast.LENGTH_LONG).show();
}
}
}
}
/* --------------------------
部分二:ThreeFingerService(前台服务 + 悬浮触摸层)
负责创建全局透明触摸层,拦截全局触摸事件并交给手势管理器
-------------------------- */
public class ThreeFingerService extends Service {
private WindowManager mWindowManager;
private View mTouchOverlay;
private ThreeFingerGestureDetector mGestureDetector;
@Override
public void onCreate() {
super.onCreate();
// 初始化 WindowManager
mWindowManager = (WindowManager)getSystemService(WINDOW_SERVICE);
// 创建透明触摸层
mTouchOverlay = new View(this);
mGestureDetector = new ThreeFingerGestureDetector(this);
// 设置触摸层属性
WindowManager.LayoutParams params = new WindowManager.LayoutParams(
WindowManager.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.MATCH_PARENT,
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
? WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
: WindowManager.LayoutParams.TYPE_PHONE,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL,
PixelFormat.TRANSLUCENT);
params.gravity = Gravity.TOP | Gravity.START;
// 监听触摸事件
mTouchOverlay.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
// 将触摸事件传给手势检测器
mGestureDetector.onTouchEvent(event);
// 如果返回 true,则拦截事件,不下发给底层
return true;
}
});
// 添加到窗口管理器
mWindowManager.addView(mTouchOverlay, params);
// 启动前台通知(Android 8.0+ 要求)
startForegroundServiceWithNotification();
}
// 创建并显示前台服务通知
private void startForegroundServiceWithNotification() {
String channelId = "three_finger_channel";
String channelName = "三指截屏服务";
NotificationManager nm = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(
channelId, channelName, NotificationManager.IMPORTANCE_LOW);
nm.createNotificationChannel(channel);
}
Notification notification = new Notification.Builder(this, channelId)
.setContentTitle("三指截屏服务已启动")
.setContentText("使用三指手势即可截屏")
.setSmallIcon(android.R.drawable.ic_menu_camera)
.build();
startForeground(1, notification);
}
@Override
public void onDestroy() {
super.onDestroy();
// 移除触摸层
if (mTouchOverlay != null) {
mWindowManager.removeView(mTouchOverlay);
}
}
@Nullable @Override
public IBinder onBind(Intent intent) {
return null;
}
}
/* --------------------------
部分三:ThreeFingerGestureDetector(手势管理类)
实现三指并拢长按和三指向下滑动手势检测
-------------------------- */
class ThreeFingerGestureDetector {
private static final String TAG = "ThreeFingerGD";
private final Context mContext;
// 存储当前触摸点信息
private int mPointerCount = 0;
private long mThreeDownTime = 0L;
private float[] mStartX = new float[3];
private float[] mStartY = new float[3];
// 手势阈值
private static final long LONG_PRESS_DURATION = 500; // ms
private static final float MAX_DISTANCE = 100f; // px
private static final float SLIDE_DISTANCE = 200f; // px
public ThreeFingerGestureDetector(Context context) {
mContext = context;
}
public void onTouchEvent(MotionEvent event) {
int action = event.getActionMasked();
switch (action) {
case MotionEvent.ACTION_POINTER_DOWN:
case MotionEvent.ACTION_DOWN:
mPointerCount = event.getPointerCount();
if (mPointerCount == 3) {
// 记录三指按下时刻和位置
mThreeDownTime = System.currentTimeMillis();
for (int i = 0; i < 3; i++) {
mStartX[i] = event.getX(i);
mStartY[i] = event.getY(i);
}
// 开始定时检测长按
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
checkLongPress();
}
}, LONG_PRESS_DURATION);
}
break;
case MotionEvent.ACTION_MOVE:
if (mPointerCount == 3) {
// 检测三指滑动
float y0 = event.getY(0), y1 = event.getY(1), y2 = event.getY(2);
float dy0 = y0 - mStartY[0], dy1 = y1 - mStartY[1], dy2 = y2 - mStartY[2];
if (dy0 > SLIDE_DISTANCE && dy1 > SLIDE_DISTANCE && dy2 > SLIDE_DISTANCE) {
Log.d(TAG, "检测到三指向下滑动截屏");
triggerScreenshot();
reset();
}
}
break;
case MotionEvent.ACTION_POINTER_UP:
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
// 手指抬起或取消,重置状态
reset();
break;
}
}
// 检查三指并拢长按
private void checkLongPress() {
if (mPointerCount == 3 &&
System.currentTimeMillis() - mThreeDownTime >= LONG_PRESS_DURATION) {
// 计算三指间最大距离
float maxDist = 0;
for (int i = 0; i < 3; i++) {
for (int j = i + 1; j < 3; j++) {
float dx = mStartX[i] - mStartX[j];
float dy = mStartY[i] - mStartY[j];
float dist = (float)Math.hypot(dx, dy);
if (dist > maxDist) maxDist = dist;
}
}
if (maxDist < MAX_DISTANCE) {
Log.d(TAG, "检测到三指并拢长按截屏");
triggerScreenshot();
}
reset();
}
}
// 调用无障碍服务截屏
private void triggerScreenshot() {
Intent intent = new Intent(mContext, MyAccessibilityService.class);
intent.setAction(MyAccessibilityService.ACTION_TRIGGER_SCREENSHOT);
mContext.sendBroadcast(intent);
}
// 重置状态
private void reset() {
mPointerCount = 0;
mThreeDownTime = 0;
}
}
/* --------------------------
部分四:MyAccessibilityService(无障碍服务)
在服务中接收广播并调用 performGlobalAction 截屏
-------------------------- */
public class MyAccessibilityService extends AccessibilityService {
public static final String ACTION_TRIGGER_SCREENSHOT =
"com.example.threefingerscreenshot.ACTION_TRIGGER_SCREENSHOT";
private BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (ACTION_TRIGGER_SCREENSHOT.equals(intent.getAction())) {
// 调用系统截屏
performGlobalAction(GLOBAL_ACTION_TAKE_SCREENSHOT);
Toast.makeText(context,
"已截屏(通过无障碍服务)", Toast.LENGTH_SHORT).show();
}
}
};
@Override
public void onServiceConnected() {
super.onServiceConnected();
// 配置服务信息
AccessibilityServiceInfo info = getServiceInfo();
info.eventTypes = AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED;
info.feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC;
info.flags = AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS;
setServiceInfo(info);
// 注册广播接收器
IntentFilter filter = new IntentFilter(ACTION_TRIGGER_SCREENSHOT);
registerReceiver(mReceiver, filter);
}
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
// 不需要额外处理界面变化
}
@Override
public void onInterrupt() {
// 服务被中断时的清理
}
@Override
public void onDestroy() {
super.onDestroy();
unregisterReceiver(mReceiver);
}
}
<!-- ========================================================================
文件:res/layout/activity_main.xml
说明:主界面仅用于提示用户操作,无具体 UI 元素,保持简洁
======================================================================== -->
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- 提示文本,可自行美化 -->
<TextView
android:id="@+id/tv_hint"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="三指截屏示例\n\n1. 请授予悬浮窗权限\n2. 请开启无障碍服务\n3. 三指并拢长按或向下滑动即可截屏"
android:gravity="center"
android:textSize="18sp"
android:textColor="#333333"/>
</FrameLayout>
<!-- ========================================================================
文件:AndroidManifest.xml
说明:声明 Activity、Service、AccessibilityService 与权限
======================================================================== -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.threefingerscreenshot">
<!-- 悬浮窗权限 -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<application
android:allowBackup="true"
android:label="ThreeFingerScreenshot"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
<!-- 主 Activity -->
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- 前台触摸服务 -->
<service android:name=".ThreeFingerService"/>
<!-- 无障碍服务 -->
<service
android:name=".MyAccessibilityService"
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/accessibility_service_config"/>
</service>
</application>
</manifest>
<!-- ========================================================================
文件:res/xml/accessibility_service_config.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:notificationTimeout="100"
android:canRetrieveWindowContent="true"
android:settingsActivity="com.example.threefingerscreenshot.SettingsActivity"
android:description="@string/accessibility_service_description"/>
四、方法解读
以下仅对上面代码中核心方法进行功能说明,不复写代码:
-
MainActivity.onCreate()
-
检查并请求 SYSTEM_ALERT_WINDOW(悬浮窗) 权限
-
提示用户开启 无障碍服务
-
启动前台服务
ThreeFingerService
-
-
ThreeFingerService.onCreate()
-
通过
WindowManager
创建全屏透明触摸层 -
拦截所有触摸事件并交由
ThreeFingerGestureDetector
处理 -
调用
startForeground()
以保持服务常驻
-
-
ThreeFingerGestureDetector.onTouchEvent(MotionEvent)
-
监听指针按下、移动和抬起事件
-
当检测到三指同时按下,记录坐标和时间,启动延时任务检测长按
-
在
ACTION_MOVE
中判断三指整体向下滑动是否超过阈值 -
如果满足任一手势(长按或滑动),调用
triggerScreenshot()
-
-
ThreeFingerGestureDetector.checkLongPress()
-
判断从按下到当前时间是否超过长按阈值
-
计算三指间最大间距,如果小于并拢阈值,则认定为并拢长按
-
-
ThreeFingerGestureDetector.triggerScreenshot()
-
通过发送广播触发
MyAccessibilityService
执行截屏
-
-
MyAccessibilityService.onServiceConnected()
-
配置无障碍服务监听参数
-
注册广播接收器,监听来自手势检测器的截屏请求
-
-
MyAccessibilityService.onReceive()
-
收到广播后调用
performGlobalAction(GLOBAL_ACTION_TAKE_SCREENSHOT)
执行系统截屏
-
五、项目总结
1. 功能回顾
-
本项目实现了基于 三指并拢长按 和 三指向下滑动 的截屏手势
-
完全基于原生 多点触摸 与 AccessibilityService,无需 ROOT
-
使用 前台服务 + 悬浮窗 拦截全局触摸,兼容各种 App 场景
2. 优势与不足
优势
-
自然直观:三根手指并拢或滑动操作简单
-
跨 App 可用:全局触摸层可在任何界面使用
-
无授权弹窗:只需一次性开启悬浮窗与无障碍权限
不足
-
电量消耗:前台服务和悬浮窗常驻,会略微增加功耗
-
手势误触:三指触摸敏感度需调优,否则在打字、玩游戏时可能误触
-
兼容性:部分定制系统对 AccessibilityService 支持不完整
3. 扩展与优化
-
手势自定义:提供 UI 界面,让用户自定义三指手势类型和阈值
-
误触保护:在检测到当前焦点为输入法时,暂时禁用三指截屏
-
截图预览与分享:截屏后弹出小窗预览,并提供一键分享、编辑
-
折叠式手势:支持三指从屏幕边缘向内滑动截屏
4. 实际应用场景
-
截图教学:在录制教程视频时,快速截取关键步骤
-
社交分享:随时截屏并分享至微信、QQ、微博
-
游戏截屏:三指滑动一气呵成,捕捉精彩瞬间