安卓基础(悬浮窗和摄像)
ACTION_MANAGE_OVERLAY_PERMISSION
的作用就是 打开系统设置的「悬浮窗权限管理页面」
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,Uri.parse("package:" + getPackageName())
);
startActivity(intent);
直接跳转目标应用的权限页
Uri.parse("package:" + getPackageName())
的作用是直接把用户带到 当前应用的悬浮窗权限设置页,而不是让用户在系统设置中手动寻找。
Settings.canDrawOverlays(this)
—— 检查有没有「贴纸许可证」
从A页面使用startActivityForResult()跳转到B页面,B页面点击返回时将新写入的值传回到A页面。
startActivityForResult
的作用
-
启动子 Activity 并等待结果
当需要从另一个 Activity 获取数据(如用户选择照片、输入文本等)时,使用此方法启动子 Activity。子 Activity 关闭后,父 Activity 的onActivityResult()
方法会被调用,接收返回的数据。 -
数据双向传递
普通startActivity
只能单向传递数据(父 → 子),而startActivityForResult
支持子 Activity 将数据回传(子 → 父)。
为什么需要请求码(Request Code)?
请求码是一个 唯一整型标识符,用于在父 Activity 中区分不同的子 Activity 请求。它的必要性体现在以下场景:
-
多个子 Activity 返回结果到同一个父 Activity
例如,父 Activity 同时启动“选择图片”和“拍照”两个子 Activity,两者都可能返回图片数据。通过不同的请求码,父 Activity 可以判断数据来源并做相应处理。 -
同一子 Activity 多次启动
若多次启动同一个子 Activity(如多次选择不同文件),请求码可帮助区分是哪一次调用返回的结果。
创建悬浮窗(Floating Window)
-
createFloatingWindow()
- 悬浮窗参数:使用
TYPE_APPLICATION_OVERLAY
类型,允许悬浮在其他应用上方。 - 视图布局:加载
R.layout.floating_window
,包含“截图”和“确认”按钮。 - 拖动逻辑:通过
OnTouchListener
实现悬浮窗的拖拽移动。 - 按钮交互:
- 截图按钮:点击后显示可调整的截图框(
showCaptureFrame()
)。 - 确认按钮:点击后执行截图(
takeScreenshot()
),并切换按钮状态。
- 截图按钮:点击后显示可调整的截图框(
- 悬浮窗参数:使用
设置按钮点击事件
截图按钮点击
captureButton.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {showCaptureFrame(); // 显示截图框confirmButton.setVisibility(View.VISIBLE); // 显示确认按钮captureButton.setVisibility(View.GONE); // 隐藏截图按钮}
});
- 功能:点击后显示截图框,并切换按钮状态,让用户确认截图区域。
确认按钮点击
confirmButton.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {takeScreenshot(); // 执行截图confirmButton.setVisibility(View.GONE); // 隐藏确认按钮captureButton.setVisibility(View.VISIBLE); // 显示截图按钮}
});
- 功能:触发截图操作,并在截图完成后恢复按钮初始状态。
showCaptureFrame()
创建截图框布局参数
captureParams = new WindowManager.LayoutParams(800, // 初始宽度(像素)600, // 初始高度(像素)WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, // 类型:系统悬浮窗WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, // 标志:不获取焦点PixelFormat.TRANSLUCENT // 透明背景
);
captureParams.gravity = Gravity.TOP | Gravity.START; // 定位基准:左上角
captureParams.x = 100; // 初始X坐标(距离屏幕左边缘100像素)
captureParams.y = 100; // 初始Y坐标(距离屏幕顶部100像素)
- TYPE_APPLICATION_OVERLAY:Android 8.0+ 的悬浮窗类型。
- FLAG_NOT_FOCUSABLE:允许用户操作穿透截图框,避免影响其他应用。
加载截图框布局
captureFrame = LayoutInflater.from(this).inflate(R.layout.capture_frame, null);
final View resizeHandle = captureFrame.findViewById(R.id.resizeHandle); // 调整大小的手柄视图
final FrameLayout captureArea = captureFrame.findViewById(R.id.captureArea); // 可拖动的区域
- 布局文件:
R.layout.capture_frame
定义截图框的UI结构(如边框、调整手柄)。 - 视图元素:
resizeHandle
:用于拖动调整截图框大小的手柄(通常位于右下角)。captureArea
:用户可拖动的区域(通常是截图框的整个区域)。
代码注释原理
// 创建ImageReader实例,用于从Surface中读取图像数据
imageReader = ImageReader.newInstance(screenWidth, // 图像宽度(屏幕宽度)screenHeight, // 图像高度(屏幕高度)PixelFormat.RGBA_8888, // 像素格式(32位RGBA,每个通道8位)1 // 缓冲区数量(仅保留最新一帧)
);// 创建VirtualDisplay虚拟显示设备,将屏幕内容投射到ImageReader的Surface
virtualDisplay = mediaProjection.createVirtualDisplay("Screenshot", // 虚拟显示名称(任意标识符)screenWidth, // 显示宽度(与屏幕一致)screenHeight, // 显示高度(与屏幕一致)screenDensity, // 显示密度(像素密度DPI)DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, // 标志位(自动镜像显示方向)imageReader.getSurface(), // 输出目标Surface(绑定到ImageReader)null, // 回调接口(此处未使用)handler // 事件处理器(指定操作线程)
);// 延迟300毫秒执行截图操作,等待屏幕内容稳定
handler.postDelayed(new Runnable() {@Overridepublic void run() {Image image = null;try {// 从ImageReader获取最新的图像对象image = imageReader.acquireLatestImage();if (image != null) {// 获取图像平面数据(RGBA格式只有一个平面)Image.Plane[] planes = image.getPlanes();// 获取第一个平面的像素缓冲区ByteBuffer buffer = planes[0].getBuffer();// 单个像素的字节跨度(RGBA_8888格式为4字节)int pixelStride = planes[0].getPixelStride();// 每行的字节跨度(可能包含填充字节)int rowStride = planes[0].getRowStride();// 计算行填充字节数(实际数据宽度与声明的宽度差异)int rowPadding = rowStride - pixelStride * screenWidth;// 创建全屏位图(考虑行填充调整实际宽度)Bitmap fullScreenBitmap = Bitmap.createBitmap(screenWidth + rowPadding / pixelStride, // 调整后的宽度screenHeight, // 高度不变Bitmap.Config.ARGB_8888 // 配置为ARGB_8888格式);// 将缓冲区数据复制到位图中fullScreenBitmap.copyPixelsFromBuffer(buffer);// 保存全屏截图到公开相册目录saveScreenshot(fullScreenBitmap, "Fullscreen_" + System.currentTimeMillis() + ".jpg", true);// 创建调试用的全屏截图副本Bitmap debugCopy = fullScreenBitmap.copy(Bitmap.Config.ARGB_8888, true);saveDebugScreenshot(debugCopy, "fullscreen_" + System.currentTimeMillis() + ".jpg");// 在调试截图上绘制用户选择的红色选框debugCopy = fullScreenBitmap.copy(Bitmap.Config.ARGB_8888, true);Canvas canvas = new Canvas(debugCopy); // 创建画布Paint paint = new Paint(); // 创建画笔paint.setColor(Color.RED); // 设置红色paint.setStyle(Paint.Style.STROKE); // 描边样式paint.setStrokeWidth(10); // 线宽10像素canvas.drawRect(captureX, captureY, // 绘制矩形框captureX + captureWidth, captureY + captureHeight, paint);saveDebugScreenshot(debugCopy, "marked_" + System.currentTimeMillis() + ".jpg");// 计算合法的裁剪区域(防止超出屏幕边界)int x = Math.max(0, captureX); // X起点不小于0int y = Math.max(0, captureY); // Y起点不小于0int width = Math.min(captureWidth, screenWidth - x); // 宽度不超过屏幕右侧int height = Math.min(captureHeight, screenHeight - y); // 高度不超过屏幕底部// 尝试裁剪指定区域Bitmap croppedBitmap = null;try {croppedBitmap = Bitmap.createBitmap(fullScreenBitmap, // 源位图x, y, // 起始坐标width, height // 裁剪尺寸);Log.d(TAG, "成功裁剪: x=" + x + ", y=" + y + ", width=" + width + ", height=" + height);} catch (IllegalArgumentException e) {// 处理非法参数异常(如负坐标或超界)Log.e(TAG, "裁剪失败: " + e.getMessage() + ", 尝试默认值");// 使用默认居中区域(800x600)width = Math.min(800, screenWidth);height = Math.min(600, screenHeight);x = (screenWidth - width) / 2; // 水平居中y = (screenHeight - height) / 2;// 垂直居中croppedBitmap = Bitmap.createBitmap(fullScreenBitmap, x, y, width, height);}// 释放全屏位图内存fullScreenBitmap.recycle();if (croppedBitmap != null) {// 保存裁剪后的截图到相册saveScreenshot(croppedBitmap, "Cropped_" + System.currentTimeMillis() + ".jpg", false);croppedBitmap.recycle(); // 释放裁剪位图内存} else {Toast.makeText(FloatingWindowService.this, "截图失败:无法裁剪图像", Toast.LENGTH_SHORT).show();}} else {Toast.makeText(FloatingWindowService.this, "截图失败:无法获取图像", Toast.LENGTH_SHORT).show();}} catch (Exception e) {// 捕获并记录所有异常Log.e(TAG, "Error capturing screen", e);Toast.makeText(FloatingWindowService.this, "截图失败: " + e.getMessage(), Toast.LENGTH_SHORT).show();} finally {// 确保资源释放if (image != null) {image.close(); // 关闭Image对象}// 恢复截图框可见if (captureFrame != null) {captureFrame.setVisibility(View.VISIBLE);}// 恢复确认按钮可见Button confirmButton = floatingView.findViewById(R.id.confirmButton);if (confirmButton != null) {confirmButton.setVisibility(View.VISIBLE);}// 释放MediaProjection相关资源releaseMediaProjection();isCapturing = false; // 重置截图状态}}
}, 300); // 延迟300毫秒执行
123
启动截图 → 初始化MediaProjection → 创建VirtualDisplay → 延迟捕获图像 → 处理像素数据 → 裁剪区域 → 保存结果 → 清理资源
123