feat: 新增安卓自截图uts插件、修改ios允许截屏禁止录屏

This commit is contained in:
2026-06-23 17:07:16 +08:00
parent c7b62b0f4d
commit d3dab30d48
14 changed files with 590 additions and 57 deletions

View File

@@ -13,8 +13,8 @@
"src" : "图片路径" "src" : "图片路径"
} }
], ],
"versionName" : "1.0.65", "versionName" : "1.0.66",
"versionCode" : 1065, "versionCode" : 1066,
"app-plus" : { "app-plus" : {
"nvueCompiler" : "weex", "nvueCompiler" : "weex",
"compatible" : { "compatible" : {

10
package-lock.json generated
View File

@@ -9,7 +9,7 @@
"version": "3.4.5", "version": "3.4.5",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"edu-core": "git+https://git.nuttyreading.com/chenghuan/edu-core.git#v1.0.14", "edu-core": "git+https://git.nuttyreading.com/chenghuan/edu-core.git#v1.0.15",
"jquery": "^3.7.1", "jquery": "^3.7.1",
"tcplayer.js": "^5.1.0" "tcplayer.js": "^5.1.0"
}, },
@@ -68,8 +68,8 @@
"integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==" "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w=="
}, },
"node_modules/edu-core": { "node_modules/edu-core": {
"version": "1.0.14", "version": "1.0.15",
"resolved": "git+https://git.nuttyreading.com/chenghuan/edu-core.git#eb9eccdc3d281fe84127a93e019650512859ff0c", "resolved": "git+https://git.nuttyreading.com/chenghuan/edu-core.git#df2306c058445de44914622bf56459f76247f5b1",
"license": "ISC" "license": "ISC"
}, },
"node_modules/es5-shim": { "node_modules/es5-shim": {
@@ -393,8 +393,8 @@
"integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==" "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w=="
}, },
"edu-core": { "edu-core": {
"version": "git+https://git.nuttyreading.com/chenghuan/edu-core.git#eb9eccdc3d281fe84127a93e019650512859ff0c", "version": "git+https://git.nuttyreading.com/chenghuan/edu-core.git#df2306c058445de44914622bf56459f76247f5b1",
"from": "edu-core@git+https://git.nuttyreading.com/chenghuan/edu-core.git#v1.0.14" "from": "edu-core@git+https://git.nuttyreading.com/chenghuan/edu-core.git#v1.0.15"
}, },
"es5-shim": { "es5-shim": {
"version": "4.6.7", "version": "4.6.7",

View File

@@ -21,7 +21,7 @@
}, },
"homepage": "https://github.com/dcloudio/hello-uniapp#readme", "homepage": "https://github.com/dcloudio/hello-uniapp#readme",
"dependencies": { "dependencies": {
"edu-core": "git+https://git.nuttyreading.com/chenghuan/edu-core.git#v1.0.14", "edu-core": "git+https://git.nuttyreading.com/chenghuan/edu-core.git#v1.0.15",
"jquery": "^3.7.1", "jquery": "^3.7.1",
"tcplayer.js": "^5.1.0" "tcplayer.js": "^5.1.0"
}, },

View File

@@ -356,11 +356,11 @@ export default {
}, },
onReady() {}, onReady() {},
onLoad() { onLoad() {
if (plus.os.name == "Android") { if (plus.os.name == "Android") {
this.isShowTaihu=true this.isShowTaihu=true
}else{ }else{
this.isShowTaihu=false this.isShowTaihu=false
} }
this.$nextTick(() => { this.$nextTick(() => {
this.getAdvertisement(); this.getAdvertisement();
}); });

View File

@@ -1,3 +1,5 @@
## 1.0.72026-06-16
- 新增 iOS setUserCaptureScreen 支持 antiRecordOnly 参数enable 为 false 且 antiRecordOnly 为 true 时,仅禁止录屏/投屏允许截屏。实现方式为进入时提前挂载安全层UITextField.isSecureTextEntry默认不保护、保留 field 实例),通过 UIScreen.isCaptured 监听录屏/投屏,仅在录屏期间翻转 isSecureTextEntry——实时显示正常、录制到的文件为黑屏平时不影响截屏录屏进行中截屏也为黑。Android/鸿蒙忽略该参数,仍为截屏+录屏一起禁止。
## 1.0.62024-11-22 ## 1.0.62024-11-22
- 修复 HarmonyOS Next 上调用 setUserCaptureScreen 报错的 Bug - 修复 HarmonyOS Next 上调用 setUserCaptureScreen 报错的 Bug
## 1.0.52024-10-14 ## 1.0.52024-10-14

View File

@@ -63,6 +63,12 @@ declare namespace UniNamespace {
*/ */
enable : boolean; enable : boolean;
/** /**
* 仅 iOS 生效。仅在 enable 为 false 时有意义:
* true: 仅禁止录屏/投屏(录屏时画面被遮挡),允许截屏;
* false/不传: 截屏与录屏一起禁止(默认行为)。
*/
antiRecordOnly ?: boolean;
/**
* 接口调用成功的回调函数 * 接口调用成功的回调函数
*/ */
// success : SetUserCaptureScreenSuccessCallback | null, // success : SetUserCaptureScreenSuccessCallback | null,

View File

@@ -1,7 +1,7 @@
{ {
"id": "uni-usercapturescreen", "id": "uni-usercapturescreen",
"displayName": "uni-usercapturescreen", "displayName": "uni-usercapturescreen",
"version": "1.0.6", "version": "1.0.7",
"description": "用户主动截屏事件监听", "description": "用户主动截屏事件监听",
"keywords": [ "keywords": [
"截屏" "截屏"

View File

@@ -12,6 +12,16 @@ class CaptureScreenTool {
static listener : UserCaptureScreenCallback | null; static listener : UserCaptureScreenCallback | null;
static secureView : UIView | null; static secureView : UIView | null;
// ===== 仅防录屏(动态切换)相关状态 =====
// 保留 UITextField使 isSecureTextEntry 可在录屏开始/结束时反复切换
static secureField : UITextField | null;
// field 的安全画布(已挂入视图层级,承载 App 根视图)
static secureCanvas : UIView | null;
// 被安全画布包裹的 App 根视图(用于卸载时还原)
static secureRootView : UIView | null;
// 是否已为"仅防录屏"挂载安全层
static recordWrapped : boolean = false;
// 监听截屏 // 监听截屏
static listenCaptureScreen(callback : UserCaptureScreenCallback | null) { static listenCaptureScreen(callback : UserCaptureScreenCallback | null) {
this.listener = callback this.listener = callback
@@ -51,28 +61,38 @@ class CaptureScreenTool {
return null return null
} }
// 应用防截屏遮罩secureView实时显示正常但被截屏/录屏捕获到的内容为黑屏)
// 不触发回调,供内部复用;注意:需在主线程调用
static applySecureViewInternal() {
if (this.secureView != null) {
// 已应用,避免重复包裹
return
}
let secureView = this.createSecureView()
let window = UTSiOS.getKeyWindow()
let rootView = window.rootViewController == null ? null : window.rootViewController!.view
if (secureView != null && rootView != null) {
let rootSuperview = rootView!.superview
if (rootSuperview != null) {
this.secureView = secureView
rootSuperview!.addSubview(secureView!)
rootView!.removeFromSuperview()
secureView!.addSubview(rootView!)
// secureView 充满父视图并随父视图宽高自适应(横竖屏、全屏切换时自动跟随)
secureView!.frame = rootSuperview!.bounds
secureView!.autoresizingMask = [UIView.AutoresizingMask.flexibleWidth, UIView.AutoresizingMask.flexibleHeight]
// rootView 充满 secureView 并自适应,避免全屏时被固定在竖屏尺寸而缩到角落
rootView!.frame = secureView!.bounds
rootView!.autoresizingMask = [UIView.AutoresizingMask.flexibleWidth, UIView.AutoresizingMask.flexibleHeight]
}
}
}
// 开启防截屏 // 开启防截屏
static onAntiScreenshot(option : SetUserCaptureScreenOptions) { static onAntiScreenshot(option : SetUserCaptureScreenOptions) {
// uts方法默认会在子线程中执行涉及 UI 操作必须在主线程中运行,通过 DispatchQueue.main.async 方法可将代码在主线程中运行 // uts方法默认会在子线程中执行涉及 UI 操作必须在主线程中运行,通过 DispatchQueue.main.async 方法可将代码在主线程中运行
DispatchQueue.main.async(execute = () : void => { DispatchQueue.main.async(execute = () : void => {
let secureView = this.createSecureView() this.applySecureViewInternal()
let window = UTSiOS.getKeyWindow()
let rootView = window.rootViewController == null ? null : window.rootViewController!.view
if (secureView != null && rootView != null) {
let rootSuperview = rootView!.superview
if (rootSuperview != null) {
this.secureView = secureView
rootSuperview!.addSubview(secureView!)
rootView!.removeFromSuperview()
secureView!.addSubview(rootView!)
// secureView 充满父视图并随父视图宽高自适应(横竖屏、全屏切换时自动跟随)
secureView!.frame = rootSuperview!.bounds
secureView!.autoresizingMask = [UIView.AutoresizingMask.flexibleWidth, UIView.AutoresizingMask.flexibleHeight]
// rootView 充满 secureView 并自适应,避免全屏时被固定在竖屏尺寸而缩到角落
rootView!.frame = secureView!.bounds
rootView!.autoresizingMask = [UIView.AutoresizingMask.flexibleWidth, UIView.AutoresizingMask.flexibleHeight]
}
}
let res: SetUserCaptureScreenSuccess = { let res: SetUserCaptureScreenSuccess = {
} }
option.success?.(res) option.success?.(res)
@@ -80,21 +100,107 @@ class CaptureScreenTool {
}) })
} }
// ===== 仅防录屏:提前挂载安全层 + 翻转 isSecureTextEntry =====
// 提前挂载安全层(应在录屏开始前调用,例如进入页面时)。
// 挂载后默认不保护isSecureTextEntry = false→ 截屏正常、实时正常。
// 注意:需在主线程调用。
static setupRecordSecureWrapper() {
if (this.recordWrapped) {
return
}
let field = new UITextField(frame = CGRect.zero)
field.isSecureTextEntry = true
// 15.1 系统 secureView 不可用;拿不到安全画布则放弃
if (field.subviews.length == 0 || UIDevice.current.systemVersion == '15.1') {
return
}
let canvas = field.subviews[0]
canvas.subviews.forEach((item) => {
item.removeFromSuperview()
})
canvas.isUserInteractionEnabled = true
let window = UTSiOS.getKeyWindow()
let rootView = window.rootViewController == null ? null : window.rootViewController!.view
if (rootView == null) {
return
}
let rootSuperview = rootView!.superview
if (rootSuperview == null) {
return
}
// 把安全画布插入原层级,并把 App 根视图移入画布
rootSuperview!.addSubview(canvas)
rootView!.removeFromSuperview()
canvas.addSubview(rootView!)
canvas.frame = rootSuperview!.bounds
canvas.autoresizingMask = [UIView.AutoresizingMask.flexibleWidth, UIView.AutoresizingMask.flexibleHeight]
rootView!.frame = canvas.bounds
rootView!.autoresizingMask = [UIView.AutoresizingMask.flexibleWidth, UIView.AutoresizingMask.flexibleHeight]
// 关键:保留 field后续才能切换 isSecureTextEntry
this.secureField = field
this.secureCanvas = canvas
this.secureRootView = rootView
this.recordWrapped = true
// 默认不保护:允许截屏、实时正常
field.isSecureTextEntry = false
}
// 切换录屏保护true=保护录屏文件黑、实时正常、此刻截屏也黑false=不保护(允许截屏)
// 注意:需在主线程调用。
static setRecordSecure(secure : boolean) {
if (this.secureField != null) {
this.secureField!.isSecureTextEntry = secure
}
}
// 卸载安全层,恢复原始视图层级。注意:需在主线程调用。
static teardownRecordSecureWrapper() {
if (!this.recordWrapped) {
return
}
if (this.secureField != null) {
this.secureField!.isSecureTextEntry = false
}
if (this.secureCanvas != null && this.secureRootView != null) {
let rootSuperview = this.secureCanvas!.superview
if (rootSuperview != null) {
rootSuperview!.addSubview(this.secureRootView!)
this.secureRootView!.frame = rootSuperview!.bounds
this.secureCanvas!.removeFromSuperview()
}
}
this.secureField = null
this.secureCanvas = null
this.secureRootView = null
this.recordWrapped = false
}
// 移除防截屏遮罩视图(不触发回调,供内部复用)
// 注意:需在主线程调用
static removeSecureViewInternal() {
if (this.secureView != null) {
let window = UTSiOS.getKeyWindow()
let rootView = window.rootViewController == null ? null : window.rootViewController!.view
if (rootView != null && this.secureView!.superview != null) {
let rootSuperview = this.secureView!.superview
if (rootSuperview != null) {
rootSuperview!.addSubview(rootView!)
this.secureView!.removeFromSuperview()
}
}
this.secureView = null
}
}
// 关闭防截屏 // 关闭防截屏
static offAntiScreenshot(option : SetUserCaptureScreenOptions) { static offAntiScreenshot(option : SetUserCaptureScreenOptions) {
DispatchQueue.main.async(execute = () : void => { DispatchQueue.main.async(execute = () : void => {
if (this.secureView != null) { this.removeSecureViewInternal()
let window = UTSiOS.getKeyWindow()
let rootView = window.rootViewController == null ? null : window.rootViewController!.view
if (rootView != null && this.secureView!.superview != null) {
let rootSuperview = this.secureView!.superview
if (rootSuperview != null) {
rootSuperview!.addSubview(rootView!)
this.secureView!.removeFromSuperview()
}
}
this.secureView = null
}
let res: SetUserCaptureScreenSuccess = { let res: SetUserCaptureScreenSuccess = {
} }
option.success?.(res) option.success?.(res)
@@ -103,6 +209,58 @@ class CaptureScreenTool {
} }
} }
/**
* 防录屏工具类:基于 UIScreen.isCaptured 监听录屏/投屏状态,动态切换安全层。
* 截屏不会触发 isCaptured因此可做到"允许截屏、仅在录屏时保护"。
*
* 实现要点(避免实时画面变黑):
* 1) 进入时(录屏开始前)就提前挂好安全层,且默认不保护 → 截屏正常、实时正常;
* 2) 仅在录屏期间翻转 isSecureTextEntry = true → 录屏文件黑、实时正常、此刻截屏也黑;
* 3) 录屏结束翻回 false → 截屏恢复正常。
*/
class ScreenRecordTool {
// 是否已注册监听
static monitoring : boolean = false;
// 开启防录屏:提前挂载安全层并监听录屏状态变化
static onAntiScreenRecord() {
DispatchQueue.main.async(execute = () : void => {
// 关键:录屏开始前就把安全层挂好(默认不保护)
CaptureScreenTool.setupRecordSecureWrapper()
if (!this.monitoring) {
this.monitoring = true
const method = Selector("screenCaptureDidChange")
NotificationCenter.default.addObserver(this, selector = method, name = UIScreen.capturedDidChangeNotification, object = null)
}
// 进入时若已经在录屏,立即按当前状态保护
this.applyIfNeeded()
})
}
// 录屏状态变化回调
@objc static screenCaptureDidChange() {
DispatchQueue.main.async(execute = () : void => {
this.applyIfNeeded()
})
}
// 根据当前录屏状态翻转安全层开关
static applyIfNeeded() {
CaptureScreenTool.setRecordSecure(UIScreen.main.isCaptured)
}
// 关闭防录屏:移除监听并卸载安全层
static offAntiScreenRecord() {
DispatchQueue.main.async(execute = () : void => {
if (this.monitoring) {
NotificationCenter.default.removeObserver(this, name = UIScreen.capturedDidChangeNotification, object = null)
this.monitoring = false
}
CaptureScreenTool.teardownRecordSecureWrapper()
})
}
}
/** /**
* 开启截图监听 * 开启截图监听
*/ */
@@ -121,21 +279,46 @@ export const offUserCaptureScreen : OffUserCaptureScreen = function (callback :
* 开启/关闭防截屏 * 开启/关闭防截屏
*/ */
export const setUserCaptureScreen : SetUserCaptureScreen = function (options : SetUserCaptureScreenOptions) { export const setUserCaptureScreen : SetUserCaptureScreen = function (options : SetUserCaptureScreenOptions) {
if (UIDevice.current.systemVersion < "13.0") { const systemVersion = UIDevice.current.systemVersion
let res = new SetUserCaptureScreenFailImpl(12001)
options.fail?.(res);
options.complete?.(res);
} else if (UIDevice.current.systemVersion == "15.1") { if (options.enable == true) {
let res = new SetUserCaptureScreenFailImpl(12010) // 允许截屏与录屏:无条件清理录屏遮罩,再移除防截屏遮罩。
options.fail?.(res); // 清理动作不受版本限制,避免遗留监听/遮罩。
options.complete?.(res); ScreenRecordTool.offAntiScreenRecord()
} else { CaptureScreenTool.offAntiScreenshot(options)
if (options.enable == true) { return
CaptureScreenTool.offAntiScreenshot(options) }
} else {
CaptureScreenTool.onAntiScreenshot(options) if (options.antiRecordOnly == true) {
// 仅禁止录屏、允许截屏:进入页面时提前挂好安全层(默认不保护),
// 用 UIScreen.isCapturediOS 11+)监听录屏/投屏,仅在录屏期间翻转 isSecureTextEntry
// —— 实时显示正常、录到的画面为黑屏,且平时不影响截屏。
// 注意secureView 在 iOS 15.1 上不可用,该版本录屏保护会失效(属系统已知问题)。
if (systemVersion < "11.0") {
let res = new SetUserCaptureScreenFailImpl(12001)
options.fail?.(res)
options.complete?.(res)
return
} }
ScreenRecordTool.onAntiScreenRecord()
let res: SetUserCaptureScreenSuccess = {
}
options.success?.(res)
options.complete?.(res)
return
}
// 默认截屏与录屏一起禁止secureView 方案,受系统版本限制)
if (systemVersion < "13.0") {
let res = new SetUserCaptureScreenFailImpl(12001)
options.fail?.(res)
options.complete?.(res)
} else if (systemVersion == "15.1") {
let res = new SetUserCaptureScreenFailImpl(12010)
options.fail?.(res)
options.complete?.(res)
} else {
CaptureScreenTool.onAntiScreenshot(options)
} }
} }

View File

@@ -49,6 +49,12 @@
*/ */
enable : boolean; enable : boolean;
/** /**
* 仅 iOS 生效。仅在 enable 为 false 时有意义:
* true: 仅禁止录屏/投屏(录屏时画面被遮挡),允许截屏;
* false/不传: 截屏与录屏一起禁止(默认行为)。
*/
antiRecordOnly ?: boolean;
/**
* 接口调用成功的回调函数 * 接口调用成功的回调函数
*/ */
// success : SetUserCaptureScreenSuccessCallback | null, // success : SetUserCaptureScreenSuccessCallback | null,

View File

@@ -0,0 +1,29 @@
export type CaptureScreenSuccess = {
/**
* 截图结果,格式为 data URLdata:image/jpeg;base64,xxxx
*/
base64 : string
}
export type CaptureScreenSuccessCallback = (res : CaptureScreenSuccess) => void
export type CaptureScreenFail = {
errCode : number,
errMsg : string
}
export type CaptureScreenFailCallback = (res : CaptureScreenFail) => void
export type CaptureScreenCompleteCallback = (res : any) => void
export type CaptureScreenOptions = {
success ?: CaptureScreenSuccessCallback,
fail ?: CaptureScreenFailCallback,
complete ?: CaptureScreenCompleteCallback
}
/**
* 截取当前应用窗口画面(含视频等硬件加速渲染内容),通过 base64 返回。
* 仅 App-Android 生效,基于 Android PixelCopy 实现。
*/
export declare const captureScreen : (options : CaptureScreenOptions) => void

View File

@@ -0,0 +1,85 @@
{
"id": "yb-screen-capture",
"displayName": "yb-screen-capture",
"version": "1.0.0",
"description": "截取当前应用窗口(含硬件加速渲染的视频画面),返回 base64",
"keywords": [
"截图",
"截屏",
"PixelCopy"
],
"repository": "",
"engines": {
"HBuilderX": "^3.7.7"
},
"dcloudext": {
"type": "uts",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "插件不采集任何数据",
"permissions": "无"
},
"npmurl": ""
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y",
"alipay": "n"
},
"client": {
"Vue": {
"vue2": "y",
"vue3": "y"
},
"App": {
"app-android": "y",
"app-ios": "n",
"app-harmony": "n"
},
"H5-mobile": {
"Safari": "n",
"Android Browser": "n",
"微信浏览器(Android)": "n",
"QQ浏览器(Android)": "n"
},
"H5-pc": {
"Chrome": "n",
"IE": "n",
"Edge": "n",
"Firefox": "n",
"Safari": "n"
},
"小程序": {
"微信": "n",
"阿里": "n",
"百度": "n",
"字节跳动": "n",
"QQ": "n",
"钉钉": "n",
"快手": "n",
"飞书": "n",
"京东": "n"
},
"快应用": {
"华为": "n",
"联盟": "n"
}
}
}
}
}

View File

@@ -0,0 +1,22 @@
# yb-screen-capture
截取当前应用窗口画面(含视频等硬件加速渲染内容),通过 base64 返回。
-**App-Android** 生效,基于 Android `PixelCopy` 实现Android 8.0 及以上能正确截取视频画面;低于 8.0 降级为 View 截图,视频区域可能为黑屏)。
- 截图范围为「当前屏幕可见区域」,不是长页面截图。
## 使用
```js
uni.captureScreen({
success: (res) => {
// res.base64 形如data:image/jpeg;base64,xxxx
console.log(res.base64)
},
fail: (err) => {
console.error(err.errCode, err.errMsg)
}
})
```
> 注意UTS 插件需要打自定义基座(标准基座无法运行)才能调试/运行。

View File

@@ -0,0 +1,127 @@
import { UTSAndroid } from "io.dcloud.uts";
import PixelCopy from "android.view.PixelCopy";
import OnPixelCopyFinishedListener from "android.view.PixelCopy.OnPixelCopyFinishedListener";
import Bitmap from "android.graphics.Bitmap";
import Canvas from "android.graphics.Canvas";
import Handler from "android.os.Handler";
import Looper from "android.os.Looper";
import Build from "android.os.Build";
import ByteArrayOutputStream from "java.io.ByteArrayOutputStream";
import Base64 from "android.util.Base64";
import {
CaptureScreen,
CaptureScreenOptions,
CaptureScreenSuccess,
CaptureScreenFail
} from "../interface.uts";
/**
* 将 Bitmap 压缩为 JPEG 并编码为 data URL 形式的 base64
*/
function encodeBitmapToBase64(bitmap : Bitmap) : string {
const baos = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, baos);
const bytes = baos.toByteArray();
const base64 = Base64.encodeToString(bytes, Base64.NO_WRAP);
bitmap.recycle();
return "data:image/jpeg;base64," + base64;
}
/**
* PixelCopy 完成监听器
*/
class PixelCopyListener extends OnPixelCopyFinishedListener {
private bitmap : Bitmap;
private options : CaptureScreenOptions;
constructor(bitmap : Bitmap, options : CaptureScreenOptions) {
super();
this.bitmap = bitmap;
this.options = options;
}
override onPixelCopyFinished(copyResult : Int) : void {
if (copyResult == PixelCopy.SUCCESS) {
const base64 = encodeBitmapToBase64(this.bitmap);
const res : CaptureScreenSuccess = { base64: base64 };
this.options.success?.(res);
this.options.complete?.(res);
} else {
this.bitmap.recycle();
const res : CaptureScreenFail = {
errCode: 12010,
errMsg: "captureScreen:fail PixelCopy copyResult=" + copyResult
};
this.options.fail?.(res);
this.options.complete?.(res);
}
}
}
/**
* 在 UI 线程执行实际截图
*/
class CaptureRunnable extends Runnable {
private options : CaptureScreenOptions;
constructor(options : CaptureScreenOptions) {
super();
this.options = options;
}
override run() : void {
try {
const activity = UTSAndroid.getUniActivity();
if (activity == null) {
const res : CaptureScreenFail = { errCode: 12010, errMsg: "captureScreen:fail activity is null" };
this.options.fail?.(res);
this.options.complete?.(res);
return;
}
// 注意UTS 中 getWindow() 返回可空类型,必须用 ! 断言,否则编译失败
const window = activity!.getWindow()!;
const decorView = window.getDecorView()!;
const width = decorView.getWidth();
const height = decorView.getHeight();
if (width <= 0 || height <= 0) {
const res : CaptureScreenFail = { errCode: 12010, errMsg: "captureScreen:fail invalid window size" };
this.options.fail?.(res);
this.options.complete?.(res);
return;
}
const bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// Android 8.0+:使用 PixelCopy 可以正确截取 SurfaceView/视频等硬件加速画面
const handler = new Handler(Looper.getMainLooper());
PixelCopy.request(window, bitmap, new PixelCopyListener(bitmap, this.options), handler);
} else {
// 低版本降级:仅能截取 View 层级内容,视频区域可能为黑屏
const canvas = new Canvas(bitmap);
decorView.draw(canvas);
const base64 = encodeBitmapToBase64(bitmap);
const res : CaptureScreenSuccess = { base64: base64 };
this.options.success?.(res);
this.options.complete?.(res);
}
} catch (e) {
const res : CaptureScreenFail = { errCode: 12010, errMsg: "captureScreen:fail " + e.toString() };
this.options.fail?.(res);
this.options.complete?.(res);
}
}
}
export const captureScreen : CaptureScreen = function (options : CaptureScreenOptions) {
const activity = UTSAndroid.getUniActivity();
if (activity == null) {
const res : CaptureScreenFail = { errCode: 12010, errMsg: "captureScreen:fail activity is null" };
options.fail?.(res);
options.complete?.(res);
return;
}
activity!.runOnUiThread(new CaptureRunnable(options));
}

View File

@@ -0,0 +1,73 @@
/**
* uni.captureScreen 成功回调参数
*/
export type CaptureScreenSuccess = {
/**
* 截图结果,格式为 data URLdata:image/jpeg;base64,xxxx
*/
base64 : string
}
/**
* uni.captureScreen 成功回调函数定义
*/
export type CaptureScreenSuccessCallback = (res : CaptureScreenSuccess) => void
/**
* uni.captureScreen 失败回调参数
*/
export type CaptureScreenFail = {
errCode : number,
errMsg : string
}
/**
* uni.captureScreen 失败回调函数定义
*/
export type CaptureScreenFailCallback = (res : CaptureScreenFail) => void
/**
* uni.captureScreen 完成回调函数定义(成功、失败都会执行)
*/
export type CaptureScreenCompleteCallback = (res : any) => void
/**
* uni.captureScreen 参数
*/
export type CaptureScreenOptions = {
/**
* 接口调用成功的回调函数
*/
success ?: CaptureScreenSuccessCallback,
/**
* 接口调用失败的回调函数
*/
fail ?: CaptureScreenFailCallback,
/**
* 接口调用结束的回调函数(调用成功、失败都会执行)
*/
complete ?: CaptureScreenCompleteCallback
}
export type CaptureScreen = (options : CaptureScreenOptions) => void
export interface Uni {
/**
* 截取当前应用窗口画面(含视频等硬件加速渲染内容),通过 base64 返回。
* 仅 App-Android 生效,基于 Android PixelCopy 实现。
*
* @param {CaptureScreenOptions} options
* @uniPlatform {
* "app": {
* "android": {
* "osVer": "8.0",
* "uniVer": "3.7.7",
* "unixVer": "3.9.0"
* }
* }
* }
* @uniVersion 3.7.7
* @uniVueVersion 2,3
*/
captureScreen(options : CaptureScreenOptions) : void
}