635 lines
16 KiB
Vue
635 lines
16 KiB
Vue
<template>
|
||
<view>
|
||
<slot />
|
||
</view>
|
||
</template>
|
||
|
||
<script lang="uts">
|
||
import { UIView } from 'UIKit'
|
||
import {
|
||
initCameraView, setCameraCallback,
|
||
applyPermission, openAppSettings, vibrate, shutterSound, recordSound, keyDestroyer, keyListener,
|
||
open, reopen, close, destroyCamera, takePhoto, takePhotoSnapshot, takeVideo, takeVideoSnapshot, stopVideo,
|
||
setZoom, setExposure, setWhiteBalance, setHdr, setFacing, setGrid, setFlash, setAudio, setSuffix, setGallery, setOrientation, setMode, setSizeSelectors,
|
||
setPreviewCorner, setPreviewRotation, isTakingVideo, isTakingPicture, isOpened
|
||
} from './index.uts'
|
||
|
||
export default {
|
||
name: "ima-camera-view",
|
||
props: {
|
||
mode: {
|
||
type: String,
|
||
default: 'picture' // video
|
||
},
|
||
widthRatio: {
|
||
type: Number,
|
||
default: 0
|
||
},
|
||
heightRatio: {
|
||
type: Number,
|
||
default: 0
|
||
},
|
||
tolerance: {
|
||
type: Number,
|
||
default: 0.1
|
||
},
|
||
previewCornerRadius: {
|
||
type: Number,
|
||
default: 0
|
||
},
|
||
previewCornerRadiusRate: {
|
||
type: Number,
|
||
default: 0
|
||
},
|
||
previewRotation: {
|
||
type: Number,
|
||
default: 0
|
||
},
|
||
whiteBalance: {
|
||
type: String,
|
||
default: 'auto'
|
||
},
|
||
hdr: {
|
||
type: String,
|
||
default: 'off'
|
||
},
|
||
facing: {
|
||
type: String,
|
||
default: 'back'
|
||
},
|
||
flash: {
|
||
type: String,
|
||
default: 'off'
|
||
},
|
||
audio: {
|
||
type: String,
|
||
default: 'on'
|
||
},
|
||
photoSuffix: {
|
||
type: String,
|
||
default: 'jpeg'
|
||
},
|
||
orientation: {
|
||
type: String,
|
||
default: 'auto'
|
||
},
|
||
grid: {
|
||
type: String,
|
||
default: 'off'
|
||
},
|
||
gridColor: {
|
||
type: String,
|
||
default: '#808080'
|
||
},
|
||
shutter: {
|
||
type: Boolean,
|
||
default: true
|
||
},
|
||
sound: {
|
||
type: String,
|
||
default: ''
|
||
},
|
||
recorder: {
|
||
type: Boolean,
|
||
default: true
|
||
},
|
||
sound2: {
|
||
type: String,
|
||
default: ''
|
||
},
|
||
vibrate: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
duration: {
|
||
type: Number,
|
||
default: 300
|
||
},
|
||
gallery: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
shortcut: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
},
|
||
data() {
|
||
return {
|
||
cameraZoom: 0,
|
||
cameraFacing: 'back',
|
||
cameraViewActivity: null,
|
||
cameraViewContext: null,
|
||
}
|
||
},
|
||
watch: {
|
||
mode: {
|
||
handler(newValue : String) {
|
||
this.changeMode(newValue)
|
||
},
|
||
immediate: false
|
||
},
|
||
whiteBalance: {
|
||
handler(newValue : String) {
|
||
this.changeWhiteBalance(newValue)
|
||
},
|
||
immediate: true
|
||
},
|
||
hdr: {
|
||
handler(newValue : String) {
|
||
this.changeHdr(newValue)
|
||
},
|
||
immediate: true
|
||
},
|
||
facing: {
|
||
handler(newValue : String) {
|
||
this.cameraFacing = newValue;
|
||
this.changeFacing(newValue)
|
||
},
|
||
immediate: false
|
||
},
|
||
flash: {
|
||
handler(newValue : String) {
|
||
this.changeFlash(newValue)
|
||
},
|
||
immediate: false
|
||
},
|
||
audio: {
|
||
handler(newValue : String) {
|
||
this.changeAudio(newValue)
|
||
},
|
||
immediate: false
|
||
},
|
||
photoSuffix: {
|
||
handler(newValue : String) {
|
||
this.changeSuffix(newValue)
|
||
},
|
||
immediate: true
|
||
},
|
||
orientation: {
|
||
handler(newValue : String) {
|
||
this.changeOrientation(newValue)
|
||
},
|
||
immediate: true
|
||
},
|
||
grid: {
|
||
handler(newValue : String) {
|
||
this.changeGrid(newValue, '#808080')
|
||
},
|
||
immediate: true
|
||
},
|
||
gallery: {
|
||
handler(newValue : Boolean) {
|
||
this.changeGallery(newValue)
|
||
},
|
||
immediate: true
|
||
},
|
||
previewCornerRadius: {
|
||
handler(newValue ?: Number) {
|
||
this.changePreviewCorner(newValue, this.previewCornerRadiusRate)
|
||
},
|
||
immediate: true
|
||
},
|
||
previewCornerRadiusRate: {
|
||
handler(newValue ?: Number) {
|
||
this.changePreviewCorner(this.previewCornerRadius, newValue)
|
||
},
|
||
immediate: true
|
||
},
|
||
previewRotation: {
|
||
handler(newValue : Number) {
|
||
this.changePreviewRotation(newValue)
|
||
},
|
||
immediate: true
|
||
},
|
||
shortcut: {
|
||
handler(newValue : Boolean) {
|
||
this.shortcutListener()
|
||
},
|
||
immediate: true
|
||
}
|
||
},
|
||
/**
|
||
* 组件涉及的事件声明,只有声明过的事件,才能被正常发送
|
||
*/
|
||
emits: [
|
||
'onCameraOpened',
|
||
'onCameraClosed',
|
||
'onPictureTaken',
|
||
'onVideoTakenStart',
|
||
'onVideoTakenProgress',
|
||
'onVideoTakenEnd',
|
||
'onFocusStart',
|
||
'onFocusEnd',
|
||
'onZoomChanged',
|
||
'onCameraChange',
|
||
'onOrientationChange',
|
||
'onCameraTakenError',
|
||
'onCameraError',
|
||
],
|
||
/**
|
||
* 规则:如果没有配置expose,则methods中的方法均对外暴露,如果配置了expose,则以expose的配置为准向外暴露
|
||
* ['publicMethod'] 含义为:只有 `publicMethod` 在实例上可用
|
||
*/
|
||
expose: [
|
||
'open',
|
||
'reopen',
|
||
'close',
|
||
'destroyCamera',
|
||
'takePhoto',
|
||
'takePhotoSnapshot',
|
||
'takeVideo',
|
||
'takeVideoSnapshot',
|
||
'stopVideo',
|
||
'changeZoom',
|
||
'changeExposure',
|
||
'changeWhiteBalance',
|
||
'changeHdr',
|
||
'changeFacing',
|
||
'changeMode',
|
||
'changeOrientation',
|
||
'changeGrid',
|
||
'changeFlash',
|
||
'changeAudio',
|
||
'changeSuffix',
|
||
'changeSizeSelectors',
|
||
'changePreviewCorner',
|
||
'changePreviewRotation',
|
||
'changeGallery',
|
||
'openAppSettings'
|
||
],
|
||
methods: {
|
||
// 加载相机视图界面
|
||
initCameraView(): UIView{
|
||
return initCameraView((success : boolean, message : string) => {
|
||
if (success) {
|
||
this.changePreviewCorner(this.previewCornerRadius, this.previewCornerRadiusRate);
|
||
this.initCamera();
|
||
} else {
|
||
console.warn(`相机初始失败或者当前设备不支持:${message}`);
|
||
}
|
||
})
|
||
},
|
||
// 加载相机权限
|
||
initCameraPermission() {
|
||
applyPermission((allRight : boolean, grantedList : Array<String>) => {
|
||
// 用户同意了全部权限
|
||
if (allRight) {
|
||
this.initCameraView();
|
||
console.log(`用户同意了全部权限: ${grantedList}`);
|
||
}
|
||
// 用户只同意了 grantedList 中的部分权限,或者有权限被拒绝
|
||
else {
|
||
console.warn(`部分权限被拒绝: ${grantedList}`);
|
||
}
|
||
}, (doNotAskAgain : boolean, grantedList : Array<String>) => {
|
||
// 用户拒绝某些权限并勾选“不再询问”
|
||
if (doNotAskAgain) {
|
||
// 跳转到当前 App 的系统设置页
|
||
openAppSettings()
|
||
console.warn(`权限被永久拒绝: ${grantedList}`);
|
||
}
|
||
})
|
||
},
|
||
openAppSettings(){
|
||
openAppSettings()
|
||
},
|
||
// 加载相机
|
||
initCamera() {
|
||
setCameraCallback((event : string, data : any) => {
|
||
switch (event) {
|
||
case "opened":
|
||
console.log("相机已打开", data)
|
||
this.$emit('onCameraOpened', data)
|
||
break
|
||
case "closed":
|
||
console.log("相机已关闭", data)
|
||
this.$emit('onCameraClosed', data)
|
||
break
|
||
case "picture":
|
||
console.log("照片", data)
|
||
this.$emit('onPictureTaken', data)
|
||
break
|
||
case "video-start":
|
||
console.log("开始录制", data)
|
||
this.$emit('onVideoTakenStart', data)
|
||
break
|
||
case "video-progress":
|
||
console.log("录制中", data)
|
||
this.$emit('onVideoTakenProgress', data)
|
||
break
|
||
case "video-end":
|
||
console.log("结束视频", data)
|
||
// this.$emit('onVideoTakenEnd', data)
|
||
break
|
||
case "video":
|
||
console.log("视频资源", data)
|
||
this.$emit('onVideoTakenEnd', data)
|
||
this.$emit('onVideoTaken', data)
|
||
break
|
||
case "focus-start":
|
||
console.log("对焦开始", data)
|
||
this.$emit('onFocusStart', data)
|
||
break
|
||
case "focus-end":
|
||
console.log("对焦结束", data)
|
||
this.$emit('onFocusEnd', data)
|
||
break
|
||
case "zoom-change":
|
||
console.log("缩放级别", data)
|
||
this.$emit('onZoomChanged', data)
|
||
break
|
||
case "camera-change":
|
||
console.log("相机设置", data)
|
||
this.$emit('onCameraChange', data)
|
||
break
|
||
case "orientation-change":
|
||
console.log("相机角度转换", data)
|
||
this.$emit('onOrientationChange', data)
|
||
break
|
||
case "take-error":
|
||
console.log("相机拍摄监测", data)
|
||
this.$emit('onCameraTakenError', data)
|
||
break
|
||
case "error":
|
||
console.log("相机出错", data)
|
||
this.$emit('onCameraError', data)
|
||
break
|
||
}
|
||
})
|
||
open();
|
||
},
|
||
// 打开摄像头预览
|
||
open() {
|
||
open();
|
||
},
|
||
// 重新打开摄像头预览(用于自己手动重启)
|
||
reopen() {
|
||
reopen();
|
||
},
|
||
// 关闭摄像头预览
|
||
close() {
|
||
close();
|
||
},
|
||
// 销毁相机
|
||
destroyCamera() {
|
||
if (this.$el != null) {
|
||
destroyCamera()
|
||
}
|
||
},
|
||
// 拍照(标准拍照流程)
|
||
takePhoto() {
|
||
this.photoSound()
|
||
takePhoto()
|
||
},
|
||
// 快照拍照(适用于快速拍照场景)
|
||
takePhotoSnapshot() {
|
||
this.photoSound()
|
||
takePhotoSnapshot()
|
||
},
|
||
// 开始录制视频,默认保存在缓存目录中,文件名为当前时间戳
|
||
takeVideo(duration ?: Number) {
|
||
const videoDuration : Number = duration ?? 0;
|
||
this.videoSound(true)
|
||
takeVideo(videoDuration)
|
||
},
|
||
// 快照方式录制视频
|
||
takeVideoSnapshot(duration ?: Number) {
|
||
const videoDuration : Number = duration ?? 0;
|
||
this.videoSound(true)
|
||
takeVideoSnapshot(videoDuration)
|
||
},
|
||
// 停止视频录制
|
||
stopVideo() {
|
||
stopVideo();
|
||
},
|
||
// 设置摄像头缩放级别,参数 zoom 是缩放倍数(浮点数)
|
||
changeZoom(zoom : Number) {
|
||
setZoom(zoom)
|
||
},
|
||
// 设置曝光模式,参数 exposure 是曝光数值(浮点数)
|
||
changeExposure(exposure : Number) {
|
||
setExposure(exposure)
|
||
},
|
||
// 设置相机白平衡
|
||
changeWhiteBalance(whiteBalance : String) {
|
||
setWhiteBalance(whiteBalance)
|
||
},
|
||
// 设置相机HDR
|
||
changeHdr(hdr : String) {
|
||
setHdr(hdr)
|
||
},
|
||
// 设置摄像头方向
|
||
changeFacing(facing : String) {
|
||
setFacing(facing)
|
||
},
|
||
// 设置相机模式
|
||
changeMode(mode : String) {
|
||
setMode(mode)
|
||
},
|
||
// 设置相机网格及颜色
|
||
changeGrid(grid : String, color : String) {
|
||
setGrid(grid, color)
|
||
},
|
||
// 设置闪光灯模式
|
||
changeFlash(flash : String) {
|
||
setFlash(flash)
|
||
},
|
||
// 设置音频
|
||
changeAudio(audio : String) {
|
||
setAudio(audio)
|
||
},
|
||
// 设置照片输出格式
|
||
changeSuffix(suffix : String) {
|
||
setSuffix(suffix)
|
||
},
|
||
// 是否将拍摄文件保存到本地可见媒体(即相册)
|
||
changeGallery(gallery : Boolean) {
|
||
setGallery(gallery)
|
||
},
|
||
// 设置相机使用设备方向
|
||
changeOrientation(orientation : String) {
|
||
setOrientation(orientation)
|
||
},
|
||
// 手动设置特定比例的方法
|
||
changeSizeSelectors(width : Number, height : Number, tolerance : Number) {
|
||
setSizeSelectors(width, height, tolerance)
|
||
},
|
||
// 设置预览圆角。radiusRate=0.5 且组件宽高相等时为圆形预览
|
||
changePreviewCorner(radius ?: Number, radiusRate ?: Number) {
|
||
let safeRadius : Number = 0
|
||
let safeRadiusRate : Number = 0
|
||
if (radius != null) {
|
||
safeRadius = radius
|
||
}
|
||
if (radiusRate != null) {
|
||
safeRadiusRate = radiusRate
|
||
}
|
||
setPreviewCorner(safeRadius, safeRadiusRate)
|
||
},
|
||
// 设置预览旋转角度(用于外接摄像头方向修正,支持 0/90/180/270)
|
||
changePreviewRotation(degrees : Number) {
|
||
setPreviewRotation(degrees)
|
||
},
|
||
// 判断是否正在录制视频
|
||
isTakingVideo() : Boolean {
|
||
return isTakingVideo();
|
||
},
|
||
// 判断是否正在拍照
|
||
isTakingPicture() : Boolean {
|
||
return isTakingPicture();
|
||
},
|
||
// 判断摄像头是否已打开
|
||
isOpened() : Boolean {
|
||
return isOpened();
|
||
},
|
||
// 拍照提示音
|
||
photoSound() {
|
||
if (this.vibrate) {
|
||
vibrate(this.duration ?? 300)
|
||
}
|
||
if (this.shutter) {
|
||
shutterSound(this.sound ?? '')
|
||
}
|
||
},
|
||
// 录像提示音
|
||
videoSound(isStart : Boolean) {
|
||
if (this.recorder) {
|
||
recordSound(isStart, this.sound2 ?? '')
|
||
}
|
||
},
|
||
// 自拍杆、快捷键监听【只针对拍照,自己可自定义成录像】
|
||
shortcutListener() {
|
||
// 如果没有启用,则无效
|
||
if (!this.shortcut) {
|
||
return
|
||
}
|
||
// DONE:按行业标准
|
||
// 基础版(action只有:down(按下)、up(松开))
|
||
const baseOptions = {
|
||
type: "base",
|
||
intercept: true
|
||
}
|
||
keyListener(baseOptions, (action : String, code : Number, name : String) => {
|
||
console.log(`按键事件: ${action} -> ${name} (${code})`)
|
||
// 拍照:快捷键(音量键上下)、蓝牙自拍杆(拍照按钮)
|
||
if ((code == 24 || code == 25) && action == 'up') {
|
||
this.takePhoto();
|
||
}
|
||
// 聚焦(加):蓝牙自拍杆(加键)
|
||
else if (code == 168 && action == 'down') {
|
||
this.cameraZoom = this.cameraZoom + 0.01;
|
||
if (this.cameraZoom >= 1) {
|
||
this.cameraZoom = 1
|
||
}
|
||
console.log('聚焦(加)', this.cameraZoom)
|
||
this.changeZoom(this.cameraZoom)
|
||
}
|
||
// 聚焦(减):蓝牙自拍杆(减键)
|
||
else if (code == 169 && action == 'down') {
|
||
this.cameraZoom = this.cameraZoom - 0.01;
|
||
if (this.cameraZoom <= 0) {
|
||
this.cameraZoom = 0
|
||
}
|
||
console.log('聚焦(减)', this.cameraZoom)
|
||
this.changeZoom(this.cameraZoom)
|
||
}
|
||
// 设置摄像头方向 :蓝牙自拍杆(开机键)
|
||
else if (code == 119 && action == 'up') {
|
||
this.cameraFacing = this.cameraFacing == 'front' ? 'back' : 'front';
|
||
console.log('设置摄像头方向', this.cameraFacing)
|
||
this.changeFacing(this.cameraFacing)
|
||
}
|
||
});
|
||
|
||
// TODO:这个需要自己实现
|
||
// 高阶版(action有:down(按下)、up(松开)、single(单击)、double(双击)、long(长按))
|
||
// const fullOptions: ListenerOptions = {
|
||
// type: "full", // 使用高阶版
|
||
// intercept: true // 是否拦截系统事件
|
||
// doubleInterval: 400, // 双击间隔 ms(默认 400)
|
||
// longPressTime; 500, // 长按时间阈值 ms(默认 500)
|
||
// }
|
||
// keyListener(fullOptions, (action : string, code : number, name : string) => {
|
||
// console.log(`按键事件: ${action} -> ${name} (${code})`)
|
||
// });
|
||
}
|
||
},
|
||
/**
|
||
* [可选实现] 组件被创建,组件第一个生命周期,
|
||
* 在内存中被占用的时候被调用,开发者可以在这里执行一些需要提前执行的初始化逻辑
|
||
*/
|
||
created() {
|
||
// 申请权限
|
||
this.initCameraPermission()
|
||
},
|
||
NVBeforeLoad() {
|
||
|
||
},
|
||
/**
|
||
* [必须实现] 创建原生View,必须定义返回值类型
|
||
* 开发者需要重点实现这个函数,声明原生组件被创建出来的过程,以及最终生成的原生组件类型
|
||
* (iOS 返回 UIView,与 Android FrameLayout 对应)
|
||
*/
|
||
NVLoad() : UIView {
|
||
// 必须在 initCameraView 之前设置,确保原生层在预览层创建时已记录旋转角度,
|
||
// 避免后续再设置时触发不必要的预览重建
|
||
setPreviewRotation(this.previewRotation)
|
||
return this.initCameraView()
|
||
},
|
||
/**
|
||
* [可选实现] 原生View已创建
|
||
*/
|
||
NVLoaded() {
|
||
// iOS:前后台自动关开相机可接入 UIApplication 通知;与 Android UTSAndroid 生命周期对齐占位
|
||
},
|
||
/**
|
||
* [可选实现] 原生View布局完成
|
||
*/
|
||
NVLayouted() {
|
||
this.changePreviewCorner(this.previewCornerRadius, this.previewCornerRadiusRate);
|
||
// 布局完成后再次确保旋转已应用(此时 props 确定已生效)
|
||
this.changePreviewRotation(this.previewRotation);
|
||
},
|
||
/**
|
||
* [可选实现] 原生View将释放
|
||
*/
|
||
NVBeforeUnload() {
|
||
},
|
||
/**
|
||
* [可选实现] 原生View已释放,这里可以做释放View之后的操作
|
||
*/
|
||
NVUnloaded() {
|
||
// 如果组件绑定了视图则需要在组件销毁时释放视图相关资源
|
||
if (this.$el != null) {
|
||
destroyCamera();
|
||
}
|
||
// 销毁自拍杆、快捷键监听
|
||
if (this.shortcut) {
|
||
keyDestroyer();
|
||
}
|
||
},
|
||
/**
|
||
* [可选实现] 组件销毁
|
||
*/
|
||
unmounted() {
|
||
// 销毁相机视图
|
||
if (this.$el != null) {
|
||
destroyCamera();
|
||
}
|
||
// 销毁自拍杆、快捷键监听
|
||
if (this.shortcut) {
|
||
keyDestroyer();
|
||
}
|
||
},
|
||
/**
|
||
* [可选实现] 自定组件布局尺寸,用于告诉排版系统,组件自身需要的宽高
|
||
* 一般情况下,组件的宽高应该是由终端系统的排版引擎决定,组件开发者不需要实现此函数
|
||
* 但是部分场景下,组件开发者需要自己维护宽高,则需要开发者重写此函数
|
||
*/
|
||
NVMeasure(size : UTSSize) : UTSSize {
|
||
return size;
|
||
}
|
||
}
|
||
</script>
|