feat:接入ima-camera-view插件,camera_capture改为nvue实现实时相机预览+连拍
This commit is contained in:
parent
448f1b1040
commit
e9d146ac74
640
components/apex-camera/apex-camera copy.uvue
Normal file
640
components/apex-camera/apex-camera copy.uvue
Normal file
@ -0,0 +1,640 @@
|
||||
<template>
|
||||
<view class="apex-camera">
|
||||
<view class="top-bar">
|
||||
<text class="nav-btn" @click="handleBack"><</text>
|
||||
<text class="title">{{ props.title }}</text>
|
||||
<text class="badge" :class="initialized ? 'badge-ready' : 'badge-pending'">
|
||||
{{ initialized ? '就绪' : '初始化中' }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="preview-area">
|
||||
<camera id="apex-camera-preview" class="preview-camera" mode="normal" resolution="high"
|
||||
:device-position="cameraPosition" :flash="flashMode" @initdone="handleCameraReady"
|
||||
@ready="handleCameraReady" @stop="handleCameraStop" @error="handleCameraNativeError" />
|
||||
|
||||
<view v-if="!initialized" class="preview-mask">
|
||||
<text class="main-copy">{{ props.subTitle }}</text>
|
||||
<text class="minor-copy">相机正在准备中,请确认系统相机权限已允许</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="status-panel">
|
||||
<text class="status-line">摄像头:{{ cameraPosition == 'front' ? '前置' : '后置' }}</text>
|
||||
<text class="status-line">闪光灯:{{ flashModeLabel }}</text>
|
||||
<text class="status-line">保存到相册:{{ saveToAlbum ? '是' : '否' }}</text>
|
||||
<text class="status-line">{{ statusText }}</text>
|
||||
<text v-if="lastPhotoPath != ''" class="status-line">最后照片:{{ lastPhotoPath }}</text>
|
||||
<text v-if="lastVideoPath != ''" class="status-line">最后视频:{{ lastVideoPath }}</text>
|
||||
</view>
|
||||
|
||||
<view class="bottom-panel">
|
||||
<view class="action-row">
|
||||
<view class="ghost-btn" @click="toggleCamera">
|
||||
<text class="ghost-btn-text">切换</text>
|
||||
</view>
|
||||
|
||||
<view class="capture-btn" @click="takePhoto">
|
||||
<view class="capture-btn-inner">
|
||||
<text class="capture-btn-text">拍照</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="record-btn" @click="recordVideo">
|
||||
<text class="record-btn-text">{{ isRecording ? '停止' : '录像' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="tool-row">
|
||||
<view class="tool-card">
|
||||
<text class="tool-title">保存到相册</text>
|
||||
<switch :checked="saveToAlbum" color="#1677ff" @change="handleSaveToAlbumChange" />
|
||||
</view>
|
||||
|
||||
<view class="tool-card flash-card">
|
||||
<text class="tool-title">闪光灯</text>
|
||||
<view class="flash-options">
|
||||
<text class="flash-option" :class="flashMode == 'off' ? 'flash-option-active' : ''"
|
||||
@click="changeFlashMode('off')">关</text>
|
||||
<text class="flash-option" :class="flashMode == 'on' ? 'flash-option-active' : ''"
|
||||
@click="changeFlashMode('on')">开</text>
|
||||
<text class="flash-option" :class="flashMode == 'auto' ? 'flash-option-active' : ''"
|
||||
@click="changeFlashMode('auto')">自动</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, nextTick, onMounted, ref } from 'vue'
|
||||
import {
|
||||
ApexCameraFlashMode,
|
||||
ApexCameraPhotoResult,
|
||||
ApexCameraPosition,
|
||||
ApexCameraProps,
|
||||
ApexCameraVideoResult
|
||||
} from './type.uts'
|
||||
|
||||
defineOptions({
|
||||
name: 'apex-camera',
|
||||
styleIsolation: 'app'
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
ready : []
|
||||
photo : [res: ApexCameraPhotoResult]
|
||||
video : [res: ApexCameraVideoResult]
|
||||
error : [message: string]
|
||||
}>()
|
||||
|
||||
const props = withDefaults(defineProps<ApexCameraProps>(), {
|
||||
title: 'UTS 相机组件',
|
||||
subTitle: '支持 Android + iOS + 鸿蒙',
|
||||
autoInit: true,
|
||||
autoBack: true,
|
||||
maxDuration: 60,
|
||||
defaultPosition: 'back',
|
||||
defaultFlashMode: 'off',
|
||||
defaultSaveToAlbum: false,
|
||||
tipLines: () : string[] => ([])
|
||||
})
|
||||
|
||||
type ApexCameraErrorDetail = {
|
||||
errMsg ?: string
|
||||
message ?: string
|
||||
msg ?: string
|
||||
}
|
||||
|
||||
type ApexCameraErrorEvent = {
|
||||
detail ?: ApexCameraErrorDetail
|
||||
errMsg ?: string
|
||||
}
|
||||
|
||||
const initialized = ref(false)
|
||||
const busy = ref(false)
|
||||
const isRecording = ref(false)
|
||||
const cameraPosition = ref<ApexCameraPosition>(props.defaultPosition)
|
||||
const flashMode = ref<ApexCameraFlashMode>(props.defaultFlashMode)
|
||||
const saveToAlbum = ref(props.defaultSaveToAlbum)
|
||||
const statusText = ref('等待相机初始化')
|
||||
const lastPhotoPath = ref('')
|
||||
const lastVideoPath = ref('')
|
||||
|
||||
const flashModeLabel = computed(() : string => {
|
||||
if (flashMode.value == 'on') {
|
||||
return '开'
|
||||
}
|
||||
if (flashMode.value == 'auto') {
|
||||
return '自动'
|
||||
}
|
||||
return '关'
|
||||
})
|
||||
|
||||
const showToast = (title : string) => {
|
||||
uni.showToast({
|
||||
title,
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
const getErrorMessage = (event : any, fallback : string) : string => {
|
||||
const errorEvent = event as ApexCameraErrorEvent | null
|
||||
if (errorEvent != null) {
|
||||
const detail = errorEvent.detail
|
||||
if (detail != null) {
|
||||
if (detail.errMsg != null && detail.errMsg != '') {
|
||||
return detail.errMsg
|
||||
}
|
||||
if (detail.message != null && detail.message != '') {
|
||||
return detail.message
|
||||
}
|
||||
if (detail.msg != null && detail.msg != '') {
|
||||
return detail.msg
|
||||
}
|
||||
}
|
||||
if (errorEvent.errMsg != null && errorEvent.errMsg != '') {
|
||||
return errorEvent.errMsg
|
||||
}
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
const ensureCameraContext = () => {
|
||||
try {
|
||||
return uni.createCameraContext()
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const saveImageIfNeeded = (path : string) : Promise<boolean> => {
|
||||
if (!saveToAlbum.value) {
|
||||
return Promise.resolve(false)
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
uni.saveImageToPhotosAlbum({
|
||||
filePath: path,
|
||||
success: () => resolve(true),
|
||||
fail: () => resolve(false)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const saveVideoIfNeeded = (path : string) : Promise<boolean> => {
|
||||
if (!saveToAlbum.value) {
|
||||
return Promise.resolve(false)
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
uni.saveVideoToPhotosAlbum({
|
||||
filePath: path,
|
||||
success: () => resolve(true),
|
||||
fail: () => resolve(false)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const bootCamera = async () => {
|
||||
await nextTick()
|
||||
const context = ensureCameraContext()
|
||||
if (context == null) {
|
||||
const message = '创建相机上下文失败'
|
||||
statusText.value = message
|
||||
emit('error', message)
|
||||
return
|
||||
}
|
||||
initialized.value = false
|
||||
statusText.value = '相机视图已创建,等待初始化完成'
|
||||
}
|
||||
|
||||
const handleCameraReady = () => {
|
||||
initialized.value = true
|
||||
statusText.value = isRecording.value ? '录像中...' : '相机已就绪'
|
||||
emit('ready')
|
||||
}
|
||||
|
||||
const handleCameraStop = () => {
|
||||
initialized.value = false
|
||||
isRecording.value = false
|
||||
statusText.value = '相机预览已停止'
|
||||
}
|
||||
|
||||
const handleCameraNativeError = (event : any) => {
|
||||
const message = getErrorMessage(event, 'camera error')
|
||||
initialized.value = false
|
||||
isRecording.value = false
|
||||
statusText.value = message
|
||||
emit('error', message)
|
||||
showToast(message)
|
||||
}
|
||||
|
||||
const changeFlashMode = (mode : ApexCameraFlashMode) => {
|
||||
if (busy.value || flashMode.value == mode) {
|
||||
return
|
||||
}
|
||||
flashMode.value = mode
|
||||
statusText.value = `闪光灯已切换为${flashModeLabel.value}`
|
||||
}
|
||||
|
||||
const toggleCamera = async () => {
|
||||
if (busy.value || isRecording.value) {
|
||||
return
|
||||
}
|
||||
cameraPosition.value = cameraPosition.value == 'front' ? 'back' : 'front'
|
||||
initialized.value = false
|
||||
statusText.value = `正在切换到${cameraPosition.value == 'front' ? '前置' : '后置'}摄像头...`
|
||||
await bootCamera()
|
||||
}
|
||||
|
||||
const takePhoto = () => {
|
||||
if (busy.value) {
|
||||
return
|
||||
}
|
||||
const context = ensureCameraContext()
|
||||
if (context == null) {
|
||||
const message = '相机上下文不可用'
|
||||
statusText.value = message
|
||||
emit('error', message)
|
||||
return
|
||||
}
|
||||
busy.value = true
|
||||
statusText.value = '正在拍照...'
|
||||
context.takePhoto({
|
||||
quality: 'high',
|
||||
success: (res) => {
|
||||
let imagePath = ''
|
||||
if (res.tempImagePath != null && res.tempImagePath != '') {
|
||||
imagePath = res.tempImagePath
|
||||
}
|
||||
if (imagePath == '') {
|
||||
const message = '拍照失败,未返回图片路径'
|
||||
statusText.value = message
|
||||
emit('error', message)
|
||||
showToast(message)
|
||||
busy.value = false
|
||||
return
|
||||
}
|
||||
saveImageIfNeeded(imagePath).then((saved : boolean) => {
|
||||
lastPhotoPath.value = imagePath
|
||||
lastVideoPath.value = ''
|
||||
statusText.value = saved ? '拍照成功,已保存到相册' : '拍照成功'
|
||||
emit('photo', {
|
||||
path: imagePath,
|
||||
width: 0,
|
||||
height: 0,
|
||||
savedToAlbum: saved
|
||||
} as ApexCameraPhotoResult)
|
||||
showToast(saved ? '拍照成功并已保存' : '拍照成功')
|
||||
busy.value = false
|
||||
}).catch(() => {
|
||||
lastPhotoPath.value = imagePath
|
||||
lastVideoPath.value = ''
|
||||
statusText.value = '拍照成功'
|
||||
emit('photo', {
|
||||
path: imagePath,
|
||||
width: 0,
|
||||
height: 0,
|
||||
savedToAlbum: false
|
||||
} as ApexCameraPhotoResult)
|
||||
busy.value = false
|
||||
})
|
||||
},
|
||||
fail: (err) => {
|
||||
const message = getErrorMessage(err, '拍照失败')
|
||||
statusText.value = message
|
||||
emit('error', message)
|
||||
showToast(message)
|
||||
busy.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const recordVideo = () => {
|
||||
if (busy.value) {
|
||||
return
|
||||
}
|
||||
const context = ensureCameraContext()
|
||||
if (context == null) {
|
||||
const message = '相机上下文不可用'
|
||||
statusText.value = message
|
||||
emit('error', message)
|
||||
return
|
||||
}
|
||||
busy.value = true
|
||||
|
||||
if (isRecording.value) {
|
||||
statusText.value = '正在结束录像...'
|
||||
context.stopRecord({
|
||||
success: (res) => {
|
||||
let videoPath = ''
|
||||
if (res.tempVideoPath != null && res.tempVideoPath != '') {
|
||||
videoPath = res.tempVideoPath
|
||||
}
|
||||
const duration = 0
|
||||
const size = 0
|
||||
saveVideoIfNeeded(videoPath).then((saved : boolean) => {
|
||||
isRecording.value = false
|
||||
lastVideoPath.value = videoPath
|
||||
lastPhotoPath.value = ''
|
||||
statusText.value = saved ? '录像完成,已保存到相册' : '录像完成'
|
||||
emit('video', {
|
||||
path: videoPath,
|
||||
duration: duration,
|
||||
size: size,
|
||||
savedToAlbum: saved
|
||||
} as ApexCameraVideoResult)
|
||||
busy.value = false
|
||||
}).catch(() => {
|
||||
isRecording.value = false
|
||||
lastVideoPath.value = videoPath
|
||||
lastPhotoPath.value = ''
|
||||
statusText.value = '录像完成'
|
||||
emit('video', {
|
||||
path: videoPath,
|
||||
duration: duration,
|
||||
size: size,
|
||||
savedToAlbum: false
|
||||
} as ApexCameraVideoResult)
|
||||
busy.value = false
|
||||
})
|
||||
},
|
||||
fail: (err) => {
|
||||
const message = getErrorMessage(err, '结束录像失败')
|
||||
statusText.value = message
|
||||
emit('error', message)
|
||||
showToast(message)
|
||||
busy.value = false
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
statusText.value = '开始录像...'
|
||||
context.startRecord({
|
||||
timeout: props.maxDuration,
|
||||
success: () => {
|
||||
isRecording.value = true
|
||||
statusText.value = '录像中,再点一次停止'
|
||||
busy.value = false
|
||||
},
|
||||
fail: (err) => {
|
||||
const message = getErrorMessage(err, '开始录像失败')
|
||||
statusText.value = message
|
||||
emit('error', message)
|
||||
showToast(message)
|
||||
busy.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleSaveToAlbumChange = (event : UniSwitchChangeEvent) => {
|
||||
saveToAlbum.value = event.detail.value
|
||||
statusText.value = saveToAlbum.value ? '已开启保存到相册' : '已关闭保存到相册'
|
||||
}
|
||||
|
||||
const handleBack = () => {
|
||||
if (!props.autoBack) {
|
||||
return
|
||||
}
|
||||
uni.navigateBack({
|
||||
fail: () => { }
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (props.autoInit) {
|
||||
bootCamera()
|
||||
}
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
bootCamera,
|
||||
takePhoto,
|
||||
recordVideo,
|
||||
toggleCamera,
|
||||
changeFlashMode
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.apex-camera {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
.top-bar {
|
||||
height: 88px;
|
||||
padding-top: 32px;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background-color: rgba(0, 0, 0, 0.95);
|
||||
}
|
||||
|
||||
.preview-area {
|
||||
flex: 1;
|
||||
min-height: 240px;
|
||||
position: relative;
|
||||
background-color: #0b0b0b;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.preview-camera {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.preview-mask {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-left: 28px;
|
||||
padding-right: 28px;
|
||||
background-color: rgba(0, 0, 0, 0.22);
|
||||
}
|
||||
|
||||
.main-copy {
|
||||
color: #fff;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.minor-copy {
|
||||
color: #f8e53d;
|
||||
font-size: 14px;
|
||||
margin-top: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
color: #fff;
|
||||
font-size: 28px;
|
||||
width: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.badge {
|
||||
min-width: 54px;
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
border-radius: 999px;
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.badge-ready {
|
||||
background-color: #3cc064;
|
||||
}
|
||||
|
||||
.badge-pending {
|
||||
background-color: #ffb020;
|
||||
}
|
||||
|
||||
.status-panel {
|
||||
padding: 12px 14px;
|
||||
background-color: #111214;
|
||||
}
|
||||
|
||||
.status-line {
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.bottom-panel {
|
||||
background-color: #0f0f11;
|
||||
padding-top: 20px;
|
||||
padding-bottom: 18px;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
.action-row {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.ghost-btn,
|
||||
.record-btn {
|
||||
width: 92px;
|
||||
height: 50px;
|
||||
border-radius: 25px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ghost-btn {
|
||||
background-color: #2c2c2e;
|
||||
}
|
||||
|
||||
.record-btn {
|
||||
background-color: #ff6b73;
|
||||
}
|
||||
|
||||
.ghost-btn-text,
|
||||
.record-btn-text {
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.capture-btn {
|
||||
width: 98px;
|
||||
height: 98px;
|
||||
border-radius: 49px;
|
||||
background-color: #e8e8e8;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.capture-btn-inner {
|
||||
width: 82px;
|
||||
height: 82px;
|
||||
border-radius: 41px;
|
||||
background-color: #fff;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: #d9d9d9;
|
||||
}
|
||||
|
||||
.capture-btn-text {
|
||||
color: #222;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.tool-row {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.tool-card {
|
||||
width: 48%;
|
||||
min-height: 74px;
|
||||
border-radius: 14px;
|
||||
padding: 12px 14px;
|
||||
background-color: #1b1b1d;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.flash-card {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.tool-title {
|
||||
color: #fff;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.flash-options {
|
||||
flex-direction: row;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.flash-option {
|
||||
min-width: 38px;
|
||||
height: 30px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
border-radius: 15px;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
line-height: 30px;
|
||||
background-color: #2e2e32;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.flash-option-active {
|
||||
color: #111;
|
||||
background-color: #f5d667;
|
||||
}
|
||||
</style>
|
||||
594
components/apex-camera/apex-camera.uvue
Normal file
594
components/apex-camera/apex-camera.uvue
Normal file
@ -0,0 +1,594 @@
|
||||
<template>
|
||||
<view class="apex-camera">
|
||||
<view class="preview-area">
|
||||
<camera id="apex-camera-preview" class="preview-camera" mode="normal" resolution="high"
|
||||
:device-position="cameraPosition" :flash="flashMode" @initdone="handleCameraReady"
|
||||
@ready="handleCameraReady" @stop="handleCameraStop" @error="handleCameraNativeError" />
|
||||
|
||||
<view class="photo-mask">
|
||||
<image class="photo-mask-image" src="../../../../assets/img/mb.png" mode="aspectFill" />
|
||||
</view>
|
||||
|
||||
<view class="preview-top-actions">
|
||||
<view class="icon-action flash-toggle-btn" @click="cycleFlashMode">
|
||||
<text class="icon-action-symbol">闪</text>
|
||||
<text class="icon-action-state">{{ flashModeShortLabel }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="!initialized && !hasReadyOnce" class="preview-mask">
|
||||
<text class="main-copy">{{ props.subTitle }}</text>
|
||||
<text class="minor-copy">相机正在准备中,请确认系统相机权限已允许</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bottom-panel">
|
||||
<view class="action-row">
|
||||
<view class="thumb-card">
|
||||
<image v-if="lastPhotoPath != ''" class="thumb-image" :src="lastPhotoPath" mode="aspectFill" />
|
||||
<text v-else class="thumb-placeholder">图</text>
|
||||
</view>
|
||||
|
||||
<view class="capture-btn" @click="takePhoto">
|
||||
<view class="capture-btn-inner">
|
||||
<text class="capture-btn-text"></text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="ghost-btn" @click="toggleCamera">
|
||||
<text class="ghost-btn-text">转</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, nextTick, onMounted, ref } from 'vue'
|
||||
import {
|
||||
ApexCameraFlashMode,
|
||||
ApexCameraPhotoResult,
|
||||
ApexCameraPosition,
|
||||
ApexCameraProps,
|
||||
ApexCameraVideoResult
|
||||
} from './type.uts'
|
||||
|
||||
defineOptions({
|
||||
name: 'apex-camera',
|
||||
styleIsolation: 'app'
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
ready : []
|
||||
photo : [res: ApexCameraPhotoResult]
|
||||
video : [res: ApexCameraVideoResult]
|
||||
error : [message: string]
|
||||
}>()
|
||||
|
||||
const props = withDefaults(defineProps<ApexCameraProps>(), {
|
||||
title: 'UTS 相机组件',
|
||||
subTitle: '支持 Android + iOS + 鸿蒙',
|
||||
autoInit: true,
|
||||
autoBack: true,
|
||||
maxDuration: 60,
|
||||
defaultPosition: 'back',
|
||||
defaultFlashMode: 'off',
|
||||
defaultSaveToAlbum: false,
|
||||
tipLines: () : string[] => ([])
|
||||
})
|
||||
|
||||
type ApexCameraErrorDetail = {
|
||||
errMsg ?: string
|
||||
message ?: string
|
||||
msg ?: string
|
||||
}
|
||||
|
||||
type ApexCameraErrorEvent = {
|
||||
detail ?: ApexCameraErrorDetail
|
||||
errMsg ?: string
|
||||
}
|
||||
|
||||
const initialized = ref(false)
|
||||
const hasReadyOnce = ref(false)
|
||||
const busy = ref(false)
|
||||
const isRecording = ref(false)
|
||||
const cameraPosition = ref<ApexCameraPosition>(props.defaultPosition)
|
||||
const flashMode = ref<ApexCameraFlashMode>(props.defaultFlashMode)
|
||||
const saveToAlbum = ref(props.defaultSaveToAlbum)
|
||||
const statusText = ref('等待相机初始化')
|
||||
const lastPhotoPath = ref('')
|
||||
const lastVideoPath = ref('')
|
||||
|
||||
const flashModeLabel = computed(() : string => {
|
||||
if (flashMode.value == 'on') {
|
||||
return '开'
|
||||
}
|
||||
if (flashMode.value == 'auto') {
|
||||
return '自动'
|
||||
}
|
||||
return '关'
|
||||
})
|
||||
|
||||
const flashModeShortLabel = computed(() : string => {
|
||||
if (flashMode.value == 'on') {
|
||||
return '开'
|
||||
}
|
||||
if (flashMode.value == 'auto') {
|
||||
return '自'
|
||||
}
|
||||
return '关'
|
||||
})
|
||||
|
||||
const showToast = (title : string) => {
|
||||
uni.showToast({
|
||||
title,
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
const getErrorMessage = (event : any, fallback : string) : string => {
|
||||
const errorEvent = event as ApexCameraErrorEvent | null
|
||||
if (errorEvent != null) {
|
||||
const detail = errorEvent.detail
|
||||
if (detail != null) {
|
||||
if (detail.errMsg != null && detail.errMsg != '') {
|
||||
return detail.errMsg
|
||||
}
|
||||
if (detail.message != null && detail.message != '') {
|
||||
return detail.message
|
||||
}
|
||||
if (detail.msg != null && detail.msg != '') {
|
||||
return detail.msg
|
||||
}
|
||||
}
|
||||
if (errorEvent.errMsg != null && errorEvent.errMsg != '') {
|
||||
return errorEvent.errMsg
|
||||
}
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
const ensureCameraContext = () => {
|
||||
try {
|
||||
return uni.createCameraContext()
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const saveImageIfNeeded = (path : string) : Promise<boolean> => {
|
||||
if (!saveToAlbum.value) {
|
||||
return Promise.resolve(false)
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
uni.saveImageToPhotosAlbum({
|
||||
filePath: path,
|
||||
success: () => resolve(true),
|
||||
fail: () => resolve(false)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const saveVideoIfNeeded = (path : string) : Promise<boolean> => {
|
||||
if (!saveToAlbum.value) {
|
||||
return Promise.resolve(false)
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
uni.saveVideoToPhotosAlbum({
|
||||
filePath: path,
|
||||
success: () => resolve(true),
|
||||
fail: () => resolve(false)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const bootCamera = async () => {
|
||||
await nextTick()
|
||||
const context = ensureCameraContext()
|
||||
if (context == null) {
|
||||
const message = '创建相机上下文失败'
|
||||
statusText.value = message
|
||||
emit('error', message)
|
||||
return
|
||||
}
|
||||
initialized.value = false
|
||||
statusText.value = '相机视图已创建,等待初始化完成'
|
||||
}
|
||||
|
||||
const handleCameraReady = () => {
|
||||
initialized.value = true
|
||||
hasReadyOnce.value = true
|
||||
statusText.value = isRecording.value ? '录像中...' : '相机已就绪'
|
||||
emit('ready')
|
||||
}
|
||||
|
||||
const handleCameraStop = () => {
|
||||
initialized.value = false
|
||||
isRecording.value = false
|
||||
statusText.value = '相机预览已停止'
|
||||
}
|
||||
|
||||
const handleCameraNativeError = (event : any) => {
|
||||
const message = getErrorMessage(event, 'camera error')
|
||||
initialized.value = false
|
||||
isRecording.value = false
|
||||
statusText.value = message
|
||||
emit('error', message)
|
||||
showToast(message)
|
||||
}
|
||||
|
||||
const changeFlashMode = (mode : ApexCameraFlashMode) => {
|
||||
if (busy.value || flashMode.value == mode) {
|
||||
return
|
||||
}
|
||||
flashMode.value = mode
|
||||
statusText.value = `闪光灯已切换为${flashModeLabel.value}`
|
||||
}
|
||||
|
||||
const cycleFlashMode = () => {
|
||||
if (flashMode.value == 'off') {
|
||||
changeFlashMode('auto')
|
||||
return
|
||||
}
|
||||
if (flashMode.value == 'auto') {
|
||||
changeFlashMode('on')
|
||||
return
|
||||
}
|
||||
changeFlashMode('off')
|
||||
}
|
||||
|
||||
const toggleCamera = async () => {
|
||||
if (busy.value || isRecording.value) {
|
||||
return
|
||||
}
|
||||
cameraPosition.value = cameraPosition.value == 'front' ? 'back' : 'front'
|
||||
initialized.value = false
|
||||
statusText.value = `正在切换到${cameraPosition.value == 'front' ? '前置' : '后置'}摄像头...`
|
||||
await bootCamera()
|
||||
}
|
||||
|
||||
const takePhoto = () => {
|
||||
if (busy.value) {
|
||||
return
|
||||
}
|
||||
const context = ensureCameraContext()
|
||||
if (context == null) {
|
||||
const message = '相机上下文不可用'
|
||||
statusText.value = message
|
||||
emit('error', message)
|
||||
return
|
||||
}
|
||||
busy.value = true
|
||||
statusText.value = '正在拍照...'
|
||||
context.takePhoto({
|
||||
quality: 'high',
|
||||
success: (res) => {
|
||||
let imagePath = ''
|
||||
if (res.tempImagePath != null && res.tempImagePath != '') {
|
||||
imagePath = res.tempImagePath
|
||||
}
|
||||
if (imagePath == '') {
|
||||
const message = '拍照失败,未返回图片路径'
|
||||
statusText.value = message
|
||||
emit('error', message)
|
||||
showToast(message)
|
||||
busy.value = false
|
||||
return
|
||||
}
|
||||
saveImageIfNeeded(imagePath).then((saved : boolean) => {
|
||||
lastPhotoPath.value = imagePath
|
||||
lastVideoPath.value = ''
|
||||
statusText.value = saved ? '拍照成功,已保存到相册' : '拍照成功'
|
||||
emit('photo', {
|
||||
path: imagePath,
|
||||
width: 0,
|
||||
height: 0,
|
||||
savedToAlbum: saved
|
||||
} as ApexCameraPhotoResult)
|
||||
showToast(saved ? '拍照成功并已保存' : '拍照成功')
|
||||
busy.value = false
|
||||
}).catch(() => {
|
||||
lastPhotoPath.value = imagePath
|
||||
lastVideoPath.value = ''
|
||||
statusText.value = '拍照成功'
|
||||
emit('photo', {
|
||||
path: imagePath,
|
||||
width: 0,
|
||||
height: 0,
|
||||
savedToAlbum: false
|
||||
} as ApexCameraPhotoResult)
|
||||
busy.value = false
|
||||
})
|
||||
},
|
||||
fail: (err) => {
|
||||
const message = getErrorMessage(err, '拍照失败')
|
||||
statusText.value = message
|
||||
emit('error', message)
|
||||
showToast(message)
|
||||
busy.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const recordVideo = () => {
|
||||
if (busy.value) {
|
||||
return
|
||||
}
|
||||
const context = ensureCameraContext()
|
||||
if (context == null) {
|
||||
const message = '相机上下文不可用'
|
||||
statusText.value = message
|
||||
emit('error', message)
|
||||
return
|
||||
}
|
||||
busy.value = true
|
||||
|
||||
if (isRecording.value) {
|
||||
statusText.value = '正在结束录像...'
|
||||
context.stopRecord({
|
||||
success: (res) => {
|
||||
let videoPath = ''
|
||||
if (res.tempVideoPath != null && res.tempVideoPath != '') {
|
||||
videoPath = res.tempVideoPath
|
||||
}
|
||||
const duration = 0
|
||||
const size = 0
|
||||
saveVideoIfNeeded(videoPath).then((saved : boolean) => {
|
||||
isRecording.value = false
|
||||
lastVideoPath.value = videoPath
|
||||
lastPhotoPath.value = ''
|
||||
statusText.value = saved ? '录像完成,已保存到相册' : '录像完成'
|
||||
emit('video', {
|
||||
path: videoPath,
|
||||
duration: duration,
|
||||
size: size,
|
||||
savedToAlbum: saved
|
||||
} as ApexCameraVideoResult)
|
||||
busy.value = false
|
||||
}).catch(() => {
|
||||
isRecording.value = false
|
||||
lastVideoPath.value = videoPath
|
||||
lastPhotoPath.value = ''
|
||||
statusText.value = '录像完成'
|
||||
emit('video', {
|
||||
path: videoPath,
|
||||
duration: duration,
|
||||
size: size,
|
||||
savedToAlbum: false
|
||||
} as ApexCameraVideoResult)
|
||||
busy.value = false
|
||||
})
|
||||
},
|
||||
fail: (err) => {
|
||||
const message = getErrorMessage(err, '结束录像失败')
|
||||
statusText.value = message
|
||||
emit('error', message)
|
||||
showToast(message)
|
||||
busy.value = false
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
statusText.value = '开始录像...'
|
||||
context.startRecord({
|
||||
timeout: props.maxDuration,
|
||||
success: () => {
|
||||
isRecording.value = true
|
||||
statusText.value = '录像中,再点一次停止'
|
||||
busy.value = false
|
||||
},
|
||||
fail: (err) => {
|
||||
const message = getErrorMessage(err, '开始录像失败')
|
||||
statusText.value = message
|
||||
emit('error', message)
|
||||
showToast(message)
|
||||
busy.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleSaveToAlbumChange = (event : UniSwitchChangeEvent) => {
|
||||
saveToAlbum.value = event.detail.value
|
||||
statusText.value = saveToAlbum.value ? '已开启保存到相册' : '已关闭保存到相册'
|
||||
}
|
||||
|
||||
const handleBack = () => {
|
||||
if (!props.autoBack) {
|
||||
return
|
||||
}
|
||||
uni.navigateBack({
|
||||
fail: () => { }
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (props.autoInit) {
|
||||
bootCamera()
|
||||
}
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
bootCamera,
|
||||
takePhoto,
|
||||
recordVideo,
|
||||
toggleCamera,
|
||||
changeFlashMode,
|
||||
cycleFlashMode
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.apex-camera {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
.preview-area {
|
||||
flex: 1;
|
||||
min-height: 240px;
|
||||
position: relative;
|
||||
background-color: #0b0b0b;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.preview-camera {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.photo-mask {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.photo-mask-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin-top: -80px;
|
||||
}
|
||||
|
||||
.preview-top-actions {
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
top: 18px;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.icon-action {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 24px;
|
||||
background-color: rgba(18, 18, 20, 0.68);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.flash-toggle-btn {
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: rgba(255, 255, 255, 0.16);
|
||||
}
|
||||
|
||||
.icon-action-symbol {
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.icon-action-state {
|
||||
color: #f5d667;
|
||||
font-size: 11px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.preview-mask {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-left: 28px;
|
||||
padding-right: 28px;
|
||||
background-color: rgba(0, 0, 0, 0.22);
|
||||
}
|
||||
|
||||
.main-copy {
|
||||
color: #fff;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.minor-copy {
|
||||
color: #f8e53d;
|
||||
font-size: 14px;
|
||||
margin-top: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.bottom-panel {
|
||||
background-color: rgba(15, 15, 17, 0.92);
|
||||
padding-top: 16px;
|
||||
padding-bottom: 16px;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
.action-row {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.thumb-card,
|
||||
.ghost-btn {
|
||||
width: 74px;
|
||||
height: 74px;
|
||||
border-radius: 22px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.thumb-card {
|
||||
background-color: #1b1b1d;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.thumb-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.thumb-placeholder {
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.ghost-btn {
|
||||
background-color: #2c2c2e;
|
||||
}
|
||||
|
||||
.ghost-btn-text {
|
||||
color: #fff;
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.capture-btn {
|
||||
width: 98px;
|
||||
height: 98px;
|
||||
border-radius: 49px;
|
||||
background-color: #e8e8e8;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.capture-btn-inner {
|
||||
width: 82px;
|
||||
height: 82px;
|
||||
border-radius: 41px;
|
||||
background-color: #fff;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: #d9d9d9;
|
||||
}
|
||||
|
||||
.capture-btn-text {
|
||||
color: #222;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
</style>
|
||||
1
components/apex-camera/index.uts
Normal file
1
components/apex-camera/index.uts
Normal file
@ -0,0 +1 @@
|
||||
export * from './type.uts'
|
||||
29
components/apex-camera/type.uts
Normal file
29
components/apex-camera/type.uts
Normal file
@ -0,0 +1,29 @@
|
||||
export type ApexCameraMode = 'photo' | 'video'
|
||||
export type ApexCameraPosition = 'front' | 'back'
|
||||
export type ApexCameraFlashMode = 'off' | 'on' | 'auto'
|
||||
|
||||
export type ApexCameraPhotoResult = {
|
||||
path : string
|
||||
width : number
|
||||
height : number
|
||||
savedToAlbum : boolean
|
||||
}
|
||||
|
||||
export type ApexCameraVideoResult = {
|
||||
path : string
|
||||
duration : number
|
||||
size : number
|
||||
savedToAlbum : boolean
|
||||
}
|
||||
|
||||
export type ApexCameraProps = {
|
||||
title : string
|
||||
subTitle : string
|
||||
tipLines : string[]
|
||||
autoInit : boolean
|
||||
autoBack : boolean
|
||||
maxDuration : number
|
||||
defaultPosition : ApexCameraPosition
|
||||
defaultFlashMode : ApexCameraFlashMode
|
||||
defaultSaveToAlbum : boolean
|
||||
}
|
||||
287
components/使用说明.md
Normal file
287
components/使用说明.md
Normal file
@ -0,0 +1,287 @@
|
||||
# apex-camera 相机组件使用文档
|
||||
|
||||
## 概述
|
||||
|
||||
`apex-camera` 是一个基于 UTS 的跨平台相机组件,支持 **Android**、**iOS** 和**鸿蒙**三端。组件封装了拍照、录像、闪光灯控制、前后摄像头切换、保存到相册等常见相机功能,开箱即用。
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
components/apex-camera/
|
||||
├── apex-camera.uvue # 简洁版风格(带照片遮罩层)
|
||||
├── apex-camera copy.uvue # 完整版风格(带顶栏、状态面板、工具面板)
|
||||
├── index.uts # 导出入口
|
||||
└── type.uts # 类型定义
|
||||
```
|
||||
|
||||
> 两个 `.uvue` 文件提供了不同的 UI 风格,功能逻辑完全一致。可按需选用其中一个。
|
||||
|
||||
## 类型定义
|
||||
|
||||
### ApexCameraProps(组件 Props)
|
||||
|
||||
```ts
|
||||
type ApexCameraProps = {
|
||||
title: string // 标题文本,默认 "UTS 相机组件"
|
||||
subTitle: string // 副标题/提示文本,默认 "支持 Android + iOS + 鸿蒙"
|
||||
tipLines: string[] // 提示信息行
|
||||
autoInit: boolean // 是否自动初始化相机,默认 true
|
||||
autoBack: boolean // 点击返回时是否自动返回上一页,默认 true
|
||||
maxDuration: number // 最大录像时长(秒),默认 60
|
||||
defaultPosition: 'front' | 'back' // 默认摄像头方向,默认 'back'
|
||||
defaultFlashMode: 'off' | 'on' | 'auto' // 默认闪光灯模式,默认 'off'
|
||||
defaultSaveToAlbum: boolean // 默认是否保存到相册,默认 false
|
||||
}
|
||||
```
|
||||
|
||||
### ApexCameraPhotoResult(拍照结果)
|
||||
|
||||
```ts
|
||||
type ApexCameraPhotoResult = {
|
||||
path: string // 照片临时路径
|
||||
width: number // 照片宽度
|
||||
height: number // 照片高度
|
||||
savedToAlbum: boolean // 是否已保存到系统相册
|
||||
}
|
||||
```
|
||||
|
||||
### ApexCameraVideoResult(录像结果)
|
||||
|
||||
```ts
|
||||
type ApexCameraVideoResult = {
|
||||
path: string // 视频临时路径
|
||||
duration: number // 视频时长
|
||||
size: number // 视频文件大小
|
||||
savedToAlbum: boolean // 是否已保存到系统相册
|
||||
}
|
||||
```
|
||||
|
||||
## Props(属性)
|
||||
|
||||
| 属性名 | 类型 | 默认值 | 说明 |
|
||||
|--------|------|--------|------|
|
||||
| `title` | `string` | `"UTS 相机组件"` | 顶栏标题(仅完整版) |
|
||||
| `subTitle` | `string` | `"支持 Android + iOS + 鸿蒙"` | 初始化遮罩层的主提示文字 |
|
||||
| `tipLines` | `string[]` | `[]` | 额外的提示信息行 |
|
||||
| `autoInit` | `boolean` | `true` | 是否在 `onMounted` 时自动启动相机 |
|
||||
| `autoBack` | `boolean` | `true` | 点击返回按钮是否自动 `navigateBack` |
|
||||
| `maxDuration` | `number` | `60` | 录像最大时长(秒) |
|
||||
| `defaultPosition` | `'front' \| 'back'` | `'back'` | 默认摄像头朝向 |
|
||||
| `defaultFlashMode` | `'off' \| 'on' \| 'auto'` | `'off'` | 默认闪光灯模式 |
|
||||
| `defaultSaveToAlbum` | `boolean` | `false` | 默认是否将结果保存到系统相册 |
|
||||
|
||||
## Events(事件)
|
||||
|
||||
| 事件名 | 参数类型 | 说明 |
|
||||
|--------|----------|------|
|
||||
| `ready` | 无 | 相机初始化完成 |
|
||||
| `photo` | `ApexCameraPhotoResult` | 拍照完成 |
|
||||
| `video` | `ApexCameraVideoResult` | 录像完成 |
|
||||
| `error` | `message: string` | 发生错误(含错误信息) |
|
||||
|
||||
## Exposed Methods(暴露方法)
|
||||
|
||||
组件通过 `defineExpose` 暴露以下方法,父组件可通过 ref 调用:
|
||||
|
||||
| 方法名 | 签名 | 说明 |
|
||||
|--------|------|------|
|
||||
| `bootCamera` | `() => Promise<void>` | 手动启动/重启相机 |
|
||||
| `takePhoto` | `() => void` | 触发拍照 |
|
||||
| `recordVideo` | `() => void` | 触发录像(再次调用停止) |
|
||||
| `toggleCamera` | `() => Promise<void>` | 切换前后摄像头 |
|
||||
| `changeFlashMode` | `(mode: 'off' \| 'on' \| 'auto') => void` | 设置闪光灯模式 |
|
||||
| `cycleFlashMode` | `() => void` | 循环切换闪光灯模式(off→auto→on→off) |
|
||||
|
||||
> 注意:`cycleFlashMode` 仅在简洁版(`apex-camera.uvue`)中暴露。
|
||||
|
||||
## 基本用法
|
||||
|
||||
### 1. 引入组件
|
||||
|
||||
在 `pages.json` 中注册组件:
|
||||
|
||||
```json
|
||||
{
|
||||
"easycom": {
|
||||
"autoscan": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
或者直接 import:
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
import ApexCamera from '@/uni_modules/apex-gu-cheng-uts/components/apex-camera/apex-camera.uvue'
|
||||
</script>
|
||||
```
|
||||
|
||||
### 2. 最简单的使用
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<apex-camera
|
||||
@photo="onPhoto"
|
||||
@video="onVideo"
|
||||
@error="onError"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const onPhoto = (res) => {
|
||||
console.log('拍照完成:', res.path)
|
||||
// res.path - 照片临时路径
|
||||
// res.savedToAlbum - 是否已保存到相册
|
||||
}
|
||||
|
||||
const onVideo = (res) => {
|
||||
console.log('录像完成:', res.path)
|
||||
// res.path - 视频临时路径
|
||||
// res.duration - 视频时长
|
||||
}
|
||||
|
||||
const onError = (message) => {
|
||||
uni.showToast({ title: message, icon: 'none' })
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### 3. 完整配置示例
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<apex-camera
|
||||
ref="cameraRef"
|
||||
title="智能拍照"
|
||||
sub-title="请将物品对准框内"
|
||||
:auto-init="true"
|
||||
:auto-back="false"
|
||||
:max-duration="30"
|
||||
default-position="back"
|
||||
default-flash-mode="auto"
|
||||
:default-save-to-album="true"
|
||||
@ready="onReady"
|
||||
@photo="onPhoto"
|
||||
@video="onVideo"
|
||||
@error="onError"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const cameraRef = ref(null)
|
||||
|
||||
const onReady = () => {
|
||||
console.log('相机已就绪')
|
||||
}
|
||||
|
||||
const onPhoto = (res) => {
|
||||
// 拍照成功,处理照片
|
||||
uni.previewImage({
|
||||
urls: [res.path]
|
||||
})
|
||||
}
|
||||
|
||||
const onVideo = (res) => {
|
||||
console.log('视频路径:', res.path)
|
||||
}
|
||||
|
||||
const onError = (msg) => {
|
||||
uni.showToast({ title: msg, icon: 'none' })
|
||||
}
|
||||
|
||||
// 可通过 ref 手动调用方法
|
||||
const manualTakePhoto = () => {
|
||||
cameraRef.value?.takePhoto()
|
||||
}
|
||||
|
||||
const switchCamera = () => {
|
||||
cameraRef.value?.toggleCamera()
|
||||
}
|
||||
|
||||
const setFlashAuto = () => {
|
||||
cameraRef.value?.changeFlashMode('auto')
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## 两种 UI 风格对比
|
||||
|
||||
### 简洁版(`apex-camera.uvue`)
|
||||
|
||||
- 全屏预览 + 照片遮罩层(可自定义遮罩图)
|
||||
- 底部一栏:缩略图 + 拍照按钮 + 切换摄像头
|
||||
- 左上角闪光灯切换按钮
|
||||
- 适合需要自定义遮罩/引导框的场景
|
||||
|
||||
### 完整版(`apex-camera copy.uvue`)
|
||||
|
||||
- 顶部导航栏:返回按钮 + 标题 + 状态徽章
|
||||
- 全屏预览 + 初始化遮罩
|
||||
- 状态面板:显示摄像头方向、闪光灯、保存状态等
|
||||
- 底部操作栏:切换 + 拍照 + 录像 + 状态文本
|
||||
- 底部工具面板:保存到相册开关 + 闪光灯三态切换
|
||||
- 适合需要完整调试信息和状态展示的场景
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **相机权限**:使用前请确保已在各平台配置相机权限。Android 需在 `AndroidManifest.xml` 中声明,iOS 需在 `Info.plist` 中添加 `NSCameraUsageDescription`。
|
||||
2. **相册保存**:如需保存到系统相册,Android 还需 `WRITE_EXTERNAL_STORAGE` 权限,iOS 需 `NSPhotoLibraryAddUsageDescription`。
|
||||
3. **临时文件**:拍照/录像返回的路径为临时文件路径,如需持久保存需自行移动或上传。
|
||||
4. **录像限制**:通过 `maxDuration` 控制最大录像时长,超出后会自动停止。
|
||||
5. **组件引用**:通过 `easycom` 自动扫描引入即可,无需手动 import。
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<view class="shoot-page">
|
||||
<ApexCamera
|
||||
class="shoot-camera"
|
||||
title="UTS 相机组件"
|
||||
sub-title="支持 Android + iOS + 鸿蒙"
|
||||
:auto-init="true"
|
||||
:auto-back="false"
|
||||
:default-save-to-album="true"
|
||||
@ready="handleReady"
|
||||
@photo="handlePhoto"
|
||||
@video="handleVideo"
|
||||
@error="handleError"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ApexCamera from '@/uni_modules/apex-gu-cheng-uts/components/apex-camera/apex-camera.uvue'
|
||||
import { ApexCameraPhotoResult, ApexCameraVideoResult } from '@/uni_modules/apex-gu-cheng-uts/components/apex-camera/type.uts'
|
||||
|
||||
const handleReady = () => {
|
||||
console.log('ApexCamera ready')
|
||||
}
|
||||
|
||||
const handlePhoto = (res : ApexCameraPhotoResult) => {
|
||||
console.log('photo result', res.path, res.width, res.height, res.savedToAlbum)
|
||||
}
|
||||
|
||||
const handleVideo = (res : ApexCameraVideoResult) => {
|
||||
console.log('video result', res.path, res.duration, res.size, res.savedToAlbum)
|
||||
}
|
||||
|
||||
const handleError = (message : string) => {
|
||||
console.error('camera error', message)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.shoot-page {
|
||||
flex: 1;
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
.shoot-camera {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
```
|
||||
@ -1 +0,0 @@
|
||||
提交下git,描述为 调整相机前指定备份
|
||||
@ -40,7 +40,10 @@
|
||||
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
|
||||
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
|
||||
"<uses-feature android:name=\"android.hardware.camera\"/>",
|
||||
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
|
||||
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.RECORD_AUDIO\"/>",
|
||||
"<uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"/>"
|
||||
]
|
||||
},
|
||||
/* ios打包配置 */
|
||||
|
||||
31
package.json
31
package.json
@ -1,6 +1,29 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"blueimp-md5": "^2.19.0",
|
||||
"js-sha256": "^0.11.1"
|
||||
}
|
||||
"id": "apex-gu-cheng-uts",
|
||||
"name": "安卓鸿蒙IOS调用相机摄像头",
|
||||
"displayName": "安卓鸿蒙IOS调用相机摄像头",
|
||||
"version": "1.0.0",
|
||||
"description": "可以在APP中实时显示摄像头预览画面,同时进行拍照录像等功能;",
|
||||
"keywords": [
|
||||
"鸿蒙",
|
||||
"安卓",
|
||||
"IOS",
|
||||
"摄像头",
|
||||
"相机"
|
||||
],
|
||||
"dcloudext": {
|
||||
"type": "component-vue",
|
||||
"sale": {
|
||||
"regular": {
|
||||
"price": "0.00"
|
||||
},
|
||||
"sourcecode": {
|
||||
"price": "0.00"
|
||||
}
|
||||
}
|
||||
},
|
||||
"uni_modules": {
|
||||
"dependencies": [],
|
||||
"encrypt": []
|
||||
}
|
||||
}
|
||||
413
pages/upload/camera_capture.nvue
Normal file
413
pages/upload/camera_capture.nvue
Normal file
@ -0,0 +1,413 @@
|
||||
<template>
|
||||
<view class="cc-page">
|
||||
<!-- 相机预览 -->
|
||||
<ima-camera-view
|
||||
ref="cameraRef"
|
||||
class="cc-camera"
|
||||
flash="off"
|
||||
facing="back"
|
||||
@onPictureTaken="onPictureTaken"
|
||||
@onCameraOpened="onCameraOpened"
|
||||
></ima-camera-view>
|
||||
|
||||
<!-- 连拍模式提示遮罩 -->
|
||||
<view class="cc-burst-overlay" v-if="isBurstMode">
|
||||
<text class="cc-burst-tip">连拍中⋯</text>
|
||||
<text class="cc-burst-count">{{ capturedList.length }} 张</text>
|
||||
<text class="cc-burst-hint">点击红色按钮停止</text>
|
||||
</view>
|
||||
|
||||
<!-- 顶部:已拍数量 & 确认 -->
|
||||
<view class="cc-topbar">
|
||||
<text class="cc-topbar-title" v-if="capturedList.length > 0">已拍 {{ capturedList.length }}/9</text>
|
||||
<text class="cc-topbar-title" v-else>相机</text>
|
||||
<text class="cc-topbar-confirm" @click="confirmCapture" :class="{ disabled: capturedList.length === 0 }">
|
||||
确认 ({{ capturedList.length }})
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作区 -->
|
||||
<view class="cc-footer">
|
||||
<!-- 返回 -->
|
||||
<view class="cc-footer-left" @click="goBack">
|
||||
<text class="cc-footer-text">取消</text>
|
||||
</view>
|
||||
|
||||
<!-- 中央拍照/停止 -->
|
||||
<view class="cc-footer-center">
|
||||
<view class="cc-stop-btn" @click="stopBurst" v-if="isBurstMode">
|
||||
<view class="cc-stop-inner">■</view>
|
||||
</view>
|
||||
<view class="cc-capture-btn" @click="capturePhoto" v-else>
|
||||
<view class="cc-capture-inner"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 右侧操作 -->
|
||||
<view class="cc-footer-right">
|
||||
<view class="cc-burst-toggle" @click="toggleBurst" v-if="!isBurstMode && capturedList.length < 9">
|
||||
<text class="cc-burst-toggle-text">⚡连拍</text>
|
||||
</view>
|
||||
<view class="cc-flip-btn" @click="flipCamera" v-if="!isBurstMode">
|
||||
<text class="cc-flip-icon">↺</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 已拍照缩略图横条 -->
|
||||
<view class="cc-thumb-bar" v-if="capturedList.length > 0">
|
||||
<scroll-view class="cc-thumb-scroll" scroll-x="true" show-scrollbar="false">
|
||||
<view class="cc-thumb-list">
|
||||
<view class="cc-thumb-item" v-for="(img, idx) in capturedList" :key="idx">
|
||||
<image class="cc-thumb-img" :src="img" mode="aspectFill"></image>
|
||||
<view class="cc-thumb-del" @click.stop="deletePhoto(idx)">
|
||||
<text class="cc-del-icon">✕</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
capturedList: [],
|
||||
cameraReady: false,
|
||||
facing: 'back',
|
||||
isBurstMode: false,
|
||||
burstTimer: null
|
||||
}
|
||||
},
|
||||
onUnload() {
|
||||
this.clearBurstTimer()
|
||||
},
|
||||
methods: {
|
||||
// 相机初始化完成
|
||||
onCameraOpened() {
|
||||
this.cameraReady = true
|
||||
console.log('相机已就绪')
|
||||
},
|
||||
|
||||
// 拍照
|
||||
takePhotoAction() {
|
||||
if (!this.cameraReady) {
|
||||
uni.showToast({ title: '相机未就绪', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.$refs.cameraRef.takePhoto()
|
||||
},
|
||||
|
||||
// 拍照完成回调
|
||||
onPictureTaken(e) {
|
||||
var path = e.detail?.path || ''
|
||||
if (path) {
|
||||
this.capturedList.push(path)
|
||||
}
|
||||
},
|
||||
|
||||
// 单拍
|
||||
capturePhoto() {
|
||||
if (this.capturedList.length >= 9) {
|
||||
uni.showToast({ title: '最多拍9张', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.takePhotoAction()
|
||||
},
|
||||
|
||||
// 连拍开关
|
||||
toggleBurst() {
|
||||
if (this.capturedList.length >= 9) {
|
||||
uni.showToast({ title: '最多拍9张', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.isBurstMode = true
|
||||
this.doBurstCapture()
|
||||
},
|
||||
|
||||
doBurstCapture() {
|
||||
if (!this.isBurstMode || this.capturedList.length >= 9) {
|
||||
this.isBurstMode = false
|
||||
return
|
||||
}
|
||||
this.takePhotoAction()
|
||||
// 间隔 1.5 秒后继续
|
||||
var that = this
|
||||
this.burstTimer = setTimeout(function() {
|
||||
that.doBurstCapture()
|
||||
}, 1500)
|
||||
},
|
||||
|
||||
stopBurst() {
|
||||
this.isBurstMode = false
|
||||
this.clearBurstTimer()
|
||||
},
|
||||
|
||||
clearBurstTimer() {
|
||||
if (this.burstTimer) {
|
||||
clearTimeout(this.burstTimer)
|
||||
this.burstTimer = null
|
||||
}
|
||||
},
|
||||
|
||||
// 切换摄像头
|
||||
flipCamera() {
|
||||
this.facing = this.facing === 'back' ? 'front' : 'back'
|
||||
this.$refs.cameraRef.changeFacing(this.facing)
|
||||
},
|
||||
|
||||
// 删除照片
|
||||
deletePhoto(idx) {
|
||||
this.capturedList.splice(idx, 1)
|
||||
},
|
||||
|
||||
// 返回
|
||||
goBack() {
|
||||
if (this.capturedList.length > 0) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定取消拍照吗?已拍照片将丢失',
|
||||
success: function(res) {
|
||||
if (res.confirm) {
|
||||
uni.navigateBack()
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
uni.navigateBack()
|
||||
}
|
||||
},
|
||||
|
||||
// 确认
|
||||
confirmCapture() {
|
||||
if (this.capturedList.length === 0) {
|
||||
uni.showToast({ title: '请先拍照', icon: 'none' })
|
||||
return
|
||||
}
|
||||
var pages = getCurrentPages()
|
||||
var prevPage = pages[pages.length - 2]
|
||||
if (prevPage) {
|
||||
prevPage.$vm.capturedPhotoList = this.capturedList
|
||||
}
|
||||
uni.navigateBack()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.cc-page {
|
||||
flex: 1;
|
||||
background-color: #000;
|
||||
position: relative;
|
||||
}
|
||||
.cc-camera {
|
||||
flex: 1;
|
||||
width: 750rpx;
|
||||
}
|
||||
/* 连拍遮罩 */
|
||||
.cc-burst-overlay {
|
||||
position: absolute;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background-color: rgba(0,0,0,0.25);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
}
|
||||
.cc-burst-tip {
|
||||
color: #fff;
|
||||
font-size: 40rpx;
|
||||
font-weight: bold;
|
||||
text-shadow: 0 2rpx 8rpx rgba(0,0,0,0.6);
|
||||
}
|
||||
.cc-burst-count {
|
||||
color: rgba(255,255,255,0.9);
|
||||
font-size: 32rpx;
|
||||
margin-top: 16rpx;
|
||||
text-shadow: 0 2rpx 8rpx rgba(0,0,0,0.6);
|
||||
}
|
||||
.cc-burst-hint {
|
||||
color: rgba(255,255,255,0.6);
|
||||
font-size: 24rpx;
|
||||
margin-top: 20rpx;
|
||||
text-shadow: 0 2rpx 8rpx rgba(0,0,0,0.6);
|
||||
}
|
||||
/* 顶部栏 */
|
||||
.cc-topbar {
|
||||
position: absolute;
|
||||
top: 0; left: 0; right: 0;
|
||||
height: 100rpx;
|
||||
padding-top: var(--status-bar-height);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-left: 30rpx;
|
||||
padding-right: 30rpx;
|
||||
z-index: 8;
|
||||
background: linear-gradient(to bottom, rgba(0,0,0,0.4), transparent);
|
||||
}
|
||||
.cc-topbar-title {
|
||||
color: #fff;
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
.cc-topbar-confirm {
|
||||
color: #409eff;
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
padding: 10rpx 24rpx;
|
||||
border-radius: 32rpx;
|
||||
background: rgba(64, 158, 255, 0.15);
|
||||
}
|
||||
.cc-topbar-confirm.disabled {
|
||||
color: #666;
|
||||
background: transparent;
|
||||
}
|
||||
/* 底部操作区 */
|
||||
.cc-footer {
|
||||
position: absolute;
|
||||
bottom: 0; left: 0; right: 0;
|
||||
height: 200rpx;
|
||||
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-left: 40rpx;
|
||||
padding-right: 40rpx;
|
||||
z-index: 8;
|
||||
background: linear-gradient(to top, rgba(0,0,0,0.5), transparent);
|
||||
}
|
||||
.cc-footer-left {
|
||||
width: 120rpx;
|
||||
}
|
||||
.cc-footer-text {
|
||||
color: #fff;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
.cc-footer-center {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.cc-footer-right {
|
||||
width: 200rpx;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 20rpx;
|
||||
}
|
||||
/* 拍照按钮 */
|
||||
.cc-capture-btn {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 50%;
|
||||
border: 6rpx solid #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.cc-capture-inner {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
}
|
||||
/* 停止按钮 */
|
||||
.cc-stop-btn {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 50%;
|
||||
border: 6rpx solid #ff4444;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255,68,68,0.15);
|
||||
}
|
||||
.cc-stop-inner {
|
||||
color: #ff4444;
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
/* 连拍按钮 */
|
||||
.cc-burst-toggle {
|
||||
padding: 12rpx 18rpx;
|
||||
border-radius: 28rpx;
|
||||
border: 2rpx solid #409eff;
|
||||
background: rgba(64,158,255,0.1);
|
||||
}
|
||||
.cc-burst-toggle-text {
|
||||
color: #409eff;
|
||||
font-size: 22rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
/* 翻转按钮 */
|
||||
.cc-flip-btn {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 50%;
|
||||
border: 2rpx solid rgba(255,255,255,0.4);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.cc-flip-icon {
|
||||
color: #fff;
|
||||
font-size: 32rpx;
|
||||
}
|
||||
/* 缩略图横条 */
|
||||
.cc-thumb-bar {
|
||||
position: absolute;
|
||||
bottom: 200rpx;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 10rpx 20rpx;
|
||||
z-index: 8;
|
||||
}
|
||||
.cc-thumb-scroll {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.cc-thumb-list {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 12rpx;
|
||||
}
|
||||
.cc-thumb-item {
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
border-radius: 8rpx;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
border: 2rpx solid rgba(255,255,255,0.3);
|
||||
flex-shrink: 0;
|
||||
background: #333;
|
||||
}
|
||||
.cc-thumb-img {
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
}
|
||||
.cc-thumb-del {
|
||||
position: absolute;
|
||||
top: -6rpx;
|
||||
right: -6rpx;
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(0,0,0,0.6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.cc-del-icon {
|
||||
color: #fff;
|
||||
font-size: 18rpx;
|
||||
}
|
||||
</style>
|
||||
@ -1,263 +0,0 @@
|
||||
<template>
|
||||
<view class="cc-page">
|
||||
<!-- H5 环境:WebRTC 摄像头预览 -->
|
||||
<view class="cc-camera-wrap" v-if="useWebRTC">
|
||||
<video id="ccVideo" ref="ccVideo" class="cc-video" autoplay playsinline muted></video>
|
||||
<canvas id="ccCanvas" ref="ccCanvas" class="cc-canvas" style="display:none;"></canvas>
|
||||
<!-- 连拍模式提示遮罩 -->
|
||||
<view class="cc-burst-overlay" v-if="isBurstMode">
|
||||
<text class="cc-burst-tip">连拍中⋯</text>
|
||||
<text class="cc-burst-count">{{ capturedList.length }} 张</text>
|
||||
<text class="cc-burst-dots">
|
||||
<text class="cc-dot" v-for="n in 3" :key="n">.</text>
|
||||
</text>
|
||||
<text class="cc-burst-hint">点击红色按钮停止</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- APP 环境:提示信息 -->
|
||||
<view class="cc-camera-wrap" v-else>
|
||||
<view class="cc-camera-hint">
|
||||
<text class="cc-hint-icon">📷</text>
|
||||
<text class="cc-hint-text">{{ isBurstMode ? '连拍中… 系统相机将自动打开' : '点击下方按钮打开相机拍照' }}</text>
|
||||
<text class="cc-hint-sub">已拍 {{ capturedList.length }} 张</text>
|
||||
</view>
|
||||
<canvas id="ccCanvas" ref="ccCanvas" class="cc-canvas" style="display:none;"></canvas>
|
||||
</view>
|
||||
|
||||
<!-- 已拍照九宫格 -->
|
||||
<view class="cc-thumb-bar">
|
||||
<view class="cc-thumb-count" v-if="capturedList.length > 0">已拍 {{ capturedList.length }} 张</view>
|
||||
<view class="cc-thumb-list">
|
||||
<view class="cc-thumb-item" v-for="(img, idx) in capturedList" :key="idx">
|
||||
<image class="cc-thumb-img" :src="img" mode="aspectFill"></image>
|
||||
<view class="cc-thumb-del" @click.stop="deletePhoto(idx)">
|
||||
<text class="cc-del-icon">✕</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="cc-thumb-item cc-thumb-placeholder" v-for="n in (9 - capturedList.length)" :key="'p'+n"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作区 -->
|
||||
<view class="cc-footer">
|
||||
<!-- 连拍模式 - 停止按钮 -->
|
||||
<view class="cc-stop-btn" @click="stopBurst" v-if="isBurstMode">
|
||||
<view class="cc-stop-inner">■</view>
|
||||
</view>
|
||||
<!-- 普通模式 - 拍照按钮 -->
|
||||
<view class="cc-capture-btn" @click="capturePhoto" v-else :class="{ disabled: capturedList.length >= 9 }">
|
||||
<view class="cc-capture-inner"></view>
|
||||
</view>
|
||||
|
||||
<view class="cc-right-actions">
|
||||
<view class="cc-burst-toggle" @click="toggleBurst" v-if="!isBurstMode && capturedList.length < 9">
|
||||
<text class="cc-burst-toggle-text">⚡连拍</text>
|
||||
</view>
|
||||
<view class="cc-confirm-btn" @click="confirmCapture" :class="{ disabled: capturedList.length === 0 }">
|
||||
<text class="cc-confirm-text">确认 ({{ capturedList.length }})</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
capturedList: [],
|
||||
mediaStream: null,
|
||||
useWebRTC: false,
|
||||
ctxReady: false,
|
||||
isBurstMode: false,
|
||||
burstTimer: null
|
||||
}
|
||||
},
|
||||
onReady() {
|
||||
this.checkEnvironment()
|
||||
},
|
||||
onUnload() {
|
||||
this.stopCamera()
|
||||
this.clearBurstTimer()
|
||||
},
|
||||
methods: {
|
||||
// 检测环境
|
||||
checkEnvironment() {
|
||||
try {
|
||||
if (typeof document !== 'undefined' && typeof navigator !== 'undefined' && navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
|
||||
this.useWebRTC = true
|
||||
this.startWebRTC()
|
||||
return
|
||||
}
|
||||
} catch (e) {}
|
||||
this.useWebRTC = false
|
||||
this.ctxReady = true
|
||||
console.log('APP环境:使用系统相机拍照')
|
||||
},
|
||||
|
||||
// H5: WebRTC 启动摄像头
|
||||
startWebRTC() {
|
||||
var that = this
|
||||
try {
|
||||
if (typeof document === 'undefined') { this.ctxReady = true; return }
|
||||
var video = document.getElementById('ccVideo')
|
||||
if (!video) { this.ctxReady = true; return }
|
||||
navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment', width: 1280, height: 720 } })
|
||||
.then(function(stream) {
|
||||
that.mediaStream = stream
|
||||
video.srcObject = stream
|
||||
video.play()
|
||||
that.ctxReady = true
|
||||
})
|
||||
.catch(function() { that.ctxReady = true })
|
||||
} catch (e) { this.ctxReady = true }
|
||||
},
|
||||
|
||||
stopCamera() {
|
||||
this.clearBurstTimer()
|
||||
if (this.mediaStream) {
|
||||
var tracks = this.mediaStream.getTracks()
|
||||
for (var i = 0; i < tracks.length; i++) { tracks[i].stop() }
|
||||
this.mediaStream = null
|
||||
}
|
||||
},
|
||||
|
||||
// 连拍开关
|
||||
toggleBurst() {
|
||||
if (this.capturedList.length >= 9) {
|
||||
uni.showToast({ title: '最多拍9张', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.isBurstMode = true
|
||||
this.doBurstCapture()
|
||||
},
|
||||
|
||||
doBurstCapture() {
|
||||
if (!this.isBurstMode || this.capturedList.length >= 9) {
|
||||
this.isBurstMode = false
|
||||
return
|
||||
}
|
||||
var that = this
|
||||
this.captureOne(function() {
|
||||
if (that.isBurstMode) {
|
||||
that.burstTimer = setTimeout(function() { that.doBurstCapture() }, 1500)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
stopBurst() {
|
||||
this.isBurstMode = false
|
||||
this.clearBurstTimer()
|
||||
},
|
||||
|
||||
clearBurstTimer() {
|
||||
if (this.burstTimer) { clearTimeout(this.burstTimer); this.burstTimer = null }
|
||||
},
|
||||
|
||||
// 单拍
|
||||
capturePhoto() {
|
||||
if (this.capturedList.length >= 9) { uni.showToast({ title: '最多拍9张', icon: 'none' }); return }
|
||||
this.captureOne()
|
||||
},
|
||||
|
||||
// 底层拍照
|
||||
captureOne(callback) {
|
||||
if (this.useWebRTC && this.ctxReady && this.mediaStream) {
|
||||
this.webRTCCapture(callback)
|
||||
} else {
|
||||
this.systemCapture(callback)
|
||||
}
|
||||
},
|
||||
|
||||
// H5: WebRTC canvas截图
|
||||
webRTCCapture(callback) {
|
||||
if (typeof document === 'undefined') { this.systemCapture(callback); return }
|
||||
var canvas = document.getElementById('ccCanvas')
|
||||
var video = document.getElementById('ccVideo')
|
||||
if (!canvas || !video) { this.systemCapture(callback); return }
|
||||
canvas.width = video.videoWidth || 1280
|
||||
canvas.height = video.videoHeight || 720
|
||||
var ctx = canvas.getContext('2d')
|
||||
ctx.drawImage(video, 0, 0, canvas.width, canvas.height)
|
||||
this.capturedList.push(canvas.toDataURL('image/jpeg', 0.85))
|
||||
if (callback) callback()
|
||||
},
|
||||
|
||||
// APP: 系统相机拍照(Android/iOS APP 都走这里)
|
||||
systemCapture(callback) {
|
||||
var that = this
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
sourceType: ['camera'],
|
||||
sizeType: ['original'],
|
||||
success: function(res) {
|
||||
if (res.tempFilePaths && res.tempFilePaths.length > 0) {
|
||||
that.capturedList.push(res.tempFilePaths[0])
|
||||
}
|
||||
if (callback) callback()
|
||||
},
|
||||
fail: function() {
|
||||
// APP上取消拍照 → 连拍模式暂停
|
||||
that.isBurstMode = false
|
||||
that.clearBurstTimer()
|
||||
if (callback) callback()
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
deletePhoto(idx) {
|
||||
this.capturedList.splice(idx, 1)
|
||||
},
|
||||
|
||||
confirmCapture() {
|
||||
if (this.capturedList.length === 0) { uni.showToast({ title: '请先拍照', icon: 'none' }); return }
|
||||
this.stopCamera()
|
||||
var pages = getCurrentPages()
|
||||
var prevPage = pages[pages.length - 2]
|
||||
if (prevPage) { prevPage.$vm.capturedPhotoList = this.capturedList }
|
||||
uni.navigateBack()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.cc-page { display:flex; flex-direction:column; height:100vh; background:#000; overflow:hidden; }
|
||||
.cc-camera-wrap { flex:1; position:relative; overflow:hidden; background:#111; display:flex; align-items:center; justify-content:center; }
|
||||
.cc-video { width:100%; height:100%; object-fit:cover; }
|
||||
.cc-canvas { display:none; }
|
||||
.cc-camera-hint { text-align:center; padding:40rpx; }
|
||||
.cc-hint-icon { font-size:80rpx; display:block; margin-bottom:20rpx; }
|
||||
.cc-hint-text { color:#999; font-size:28rpx; }
|
||||
.cc-hint-sub { color:#666; font-size:24rpx; margin-top:10rpx; }
|
||||
|
||||
.cc-burst-overlay { position:absolute; top:0;left:0;right:0;bottom:0; background:rgba(0,0,0,0.25); display:flex; flex-direction:column; align-items:center; justify-content:center; z-index:5; pointer-events:none; }
|
||||
.cc-burst-tip { color:#fff; font-size:40rpx; font-weight:bold; text-shadow:0 2rpx 8rpx rgba(0,0,0,0.6); }
|
||||
.cc-burst-count { color:rgba(255,255,255,0.9); font-size:32rpx; margin-top:16rpx; text-shadow:0 2rpx 8rpx rgba(0,0,0,0.6); }
|
||||
.cc-burst-dots { color:#409eff; font-size:60rpx; margin-top:10rpx; animation:burstBlink 0.8s ease-in-out infinite; }
|
||||
@keyframes burstBlink { 0%,100%{opacity:0.3} 50%{opacity:1} }
|
||||
.cc-burst-hint { color:rgba(255,255,255,0.6); font-size:24rpx; margin-top:20rpx; text-shadow:0 2rpx 8rpx rgba(0,0,0,0.6); }
|
||||
.cc-dot { margin:0 4rpx; }
|
||||
|
||||
.cc-thumb-bar { background:#1a1a1a; padding:16rpx 20rpx; }
|
||||
.cc-thumb-count { color:#999; font-size:22rpx; margin-bottom:12rpx; }
|
||||
.cc-thumb-list { display:flex; flex-wrap:wrap; gap:10rpx; }
|
||||
.cc-thumb-item { width:100rpx; height:100rpx; border-radius:10rpx; overflow:hidden; position:relative; background:#333; }
|
||||
.cc-thumb-img { width:100%; height:100%; }
|
||||
.cc-thumb-del { position:absolute; top:-6rpx;right:-6rpx; width:32rpx;height:32rpx; border-radius:50%; background:rgba(0,0,0,0.6); display:flex; align-items:center; justify-content:center; }
|
||||
.cc-del-icon { color:#fff; font-size:18rpx; }
|
||||
.cc-thumb-placeholder { border:2rpx dashed #444; box-sizing:border-box; }
|
||||
.cc-footer { display:flex; justify-content:space-between; align-items:center; padding:30rpx 40rpx; padding-bottom:calc(30rpx + constant(safe-area-inset-bottom)); padding-bottom:calc(30rpx + env(safe-area-inset-bottom)); background:#1a1a1a; }
|
||||
.cc-capture-btn { width:80rpx;height:80rpx; border-radius:50%; border:6rpx solid #fff; display:flex; align-items:center; justify-content:center; flex-shrink:0; }
|
||||
.cc-capture-inner { width:64rpx;height:64rpx; border-radius:50%; background:#fff; }
|
||||
.cc-capture-btn:active .cc-capture-inner { background:#ccc; }
|
||||
.cc-capture-btn.disabled { opacity:0.4; }
|
||||
.cc-stop-btn { width:80rpx;height:80rpx; border-radius:50%; border:6rpx solid #ff4444; display:flex; align-items:center; justify-content:center; flex-shrink:0; background:rgba(255,68,68,0.15); }
|
||||
.cc-stop-inner { color:#ff4444; font-size:32rpx; font-weight:bold; }
|
||||
.cc-right-actions { display:flex; align-items:center; gap:20rpx; }
|
||||
.cc-burst-toggle { padding:16rpx 24rpx; border-radius:32rpx; border:2rpx solid #409eff; background:transparent; }
|
||||
.cc-burst-toggle-text { color:#409eff; font-size:24rpx; font-weight:bold; }
|
||||
.cc-confirm-btn { padding:18rpx 40rpx; border-radius:40rpx; background:#409eff; }
|
||||
.cc-confirm-btn.disabled { background:#555; }
|
||||
.cc-confirm-text { color:#fff; font-size:28rpx; font-weight:bold; }
|
||||
</style>
|
||||
438
uni_modules/ima-camera-view/changelog.md
Normal file
438
uni_modules/ima-camera-view/changelog.md
Normal file
@ -0,0 +1,438 @@
|
||||
## 1.2.2(2026-06-11)
|
||||
> 新增功能
|
||||
|
||||
- 1、**iOS 端补齐 `previewRotation` 支持**:`index.vue` 新增 `previewRotation` prop 与 `changePreviewRotation` 方法,`index.uts` 导出 `setPreviewRotation`,`ImaCameraManager.swift` 通过 `CGAffineTransform(rotationAngle:)` 实现预览层旋转(与 Android 行为对齐)。
|
||||
|
||||
> 问题修复
|
||||
|
||||
- 1、**修复首次进入时 `previewRotation` 初始值 90° 无效**:`initCameraView()` 现在始终使用 `TextureView` 预览模式(不再等 `previewRotationDegrees != 0f` 才切换),避免初始化时用 `SurfaceView`、后续 prop 到达时 `setPreview(TEXTURE)` 触发相机重启导致旋转丢失。
|
||||
- 2、**修复 `setPreviewRotation(0)` 不生效**:`applyPreviewRotation()` 移除 `previewRotationDegrees == 0f` 提前返回,0° 时显式重置 `cv.rotation = 0f` 及 `scaleX/Y = 1f`。
|
||||
- 3、**修复旋转切换按钮点击无效**:`NVLoad()` 中在 `initCameraView()` 之前调用 `setPreviewRotation(this.previewRotation)`,确保原生 CameraView 创建时已读取旋转角度;`NVLayouted()` 增加安全网再次应用旋转。
|
||||
- 4、**修复首次进入时 `onCameraOpened` 中 `$refs.cameraRef` 可能未就绪**:使用 `$nextTick` 延迟调用 `applyPreviewRotation()`;picture→video 模式切换延迟从 300ms 延长至 800ms,并在切换前同步调用一次旋转。
|
||||
## 1.2.1(2026-06-10)
|
||||
> 新增功能
|
||||
|
||||
- 1、**外接摄像头预览旋转修正(Android)**:新增 `previewRotation` 属性及 `changePreviewRotation` 方法,用于修正无内置摄像头设备(如平板)外接 USB 摄像头时预览画面方向错误的问题;支持 `0` / `90` / `180` / `270`(或 `-90` 等等效角度)。
|
||||
- 2、设备旋转时自动保持预览修正方向不变(在 `onCameraOpened`、`onOrientationChanged` 时重新应用)。
|
||||
|
||||
> 问题修复
|
||||
|
||||
- 1、修复设置 `previewRotation` 后预览已正常、但**拍照输出图片方向仍不正确**的问题:拍照时会将 `previewRotation` 叠加到原生 `result.rotation` 后一并修正。
|
||||
|
||||
> 功能优化
|
||||
|
||||
- 1、启用 `previewRotation` 时自动切换为 `TextureView` 预览模式,并通过 `CameraView` 视图层旋转 + 缩放填充实现稳定预览(不依赖易被原生覆盖的 `TextureView.setTransform`)。
|
||||
|
||||
> 平台说明
|
||||
|
||||
- 本能力目前仅在 **`android`** 端实现;`ios` / `harmony` 暂未提供同等 API。
|
||||
|
||||
> 由于 **`harmony端`** 不支持兼容性组件,`harmony端`(指的是`harmony 5.0+`)不再本组件中更新,插件正在更新中
|
||||
## 1.2.0(2026-05-24)
|
||||
> 新增功能
|
||||
|
||||
- 无
|
||||
|
||||
> 问题修复
|
||||
|
||||
- 无
|
||||
|
||||
> 功能优化
|
||||
|
||||
- 无
|
||||
|
||||
> 由于 **`harmony端`** 不支持兼容性组件,`harmony端`(指的是`harmony 5.0+`)不再本组件中更新,插件正在更新中
|
||||
## 1.1.16(2026-05-20)
|
||||
> 新增功能
|
||||
|
||||
- 1、**预览圆角**:支持 `previewCornerRadius`、`previewCornerRadiusRate` 及 `changePreviewCorner`(圆形/多档圆角预览以原生实现为准)。
|
||||
|
||||
> 问题修复
|
||||
|
||||
- 1、修复「同页先预热 `video` 再轻触拍照」时,若在原生 `opened` 回调中无条件按 prop 再次 `changeMode`,会与原生「为拍照切换到 PICTURE 再在下一帧 `takePicture`」的逻辑冲突,导致预览反复关闭/打开、拍照异常的问题(已移除该盲目同步)。
|
||||
- 2、`nvue` / 部分运行时下 `$refs` 实例上 **`changeMode` 等方法不可用** 的问题:通过 **`mode` prop + `watch`** 同步原生模式,页面侧推荐使用 `:mode` 切换 `picture` / `video`。
|
||||
|
||||
> 功能优化
|
||||
|
||||
- 1、**`mode` prop** 增加监听:变更时调用原生 `setMode`,与 `changeMode` 行为对齐。
|
||||
- 2、录像相关:停止录像、开始时间戳等逻辑在主线程/时序上做了加固。
|
||||
|
||||
> 计划新增功能(大版本更新【即 `1.2.0` 开始】)
|
||||
## 1.1.15(2026-02-02)
|
||||
> 新增功能(临时更新)
|
||||
|
||||
- 无
|
||||
|
||||
> 问题修复
|
||||
|
||||
- 1、部分手机无法录像的问题或者录像中没有返回结果的问题(可能解决`{ "errorCode" => 5, "reason" => "录像失败", "message" => "java.lang.RuntimeException: start failed." }`的问题)
|
||||
- 1.1、增加了待录像(切换 VIDEO 模式后延后执行)
|
||||
- 1.2、待拍照(切换 PICTURE 模式后延后执行)
|
||||
|
||||
> 功能优化
|
||||
|
||||
- 1、增加拍照、录像、结束录像时异常抛出
|
||||
|
||||
> 计划新增功能(大版本更新【即 `1.2.0` 开始】)
|
||||
|
||||
## 1.1.14(2026-01-30)
|
||||
> 新增功能(临时更新)
|
||||
|
||||
- 无
|
||||
|
||||
> 问题修复
|
||||
|
||||
- 1、部分手机无法录像的问题或者录像中没有返回结果的问题
|
||||
- 2、修改代码结构,准备改成兼容组件模式,为`Harmony Next`做准备
|
||||
|
||||
> 功能优化
|
||||
|
||||
- 无
|
||||
|
||||
> 计划新增功能(大版本更新【即 `1.2.0` 开始】)
|
||||
## 1.1.13(2026-01-22)
|
||||
> 新增功能(临时更新)
|
||||
|
||||
- 无
|
||||
|
||||
> 问题修复
|
||||
|
||||
- 1、修复获取Activity实例、Application上下文上下文出现的问题!
|
||||
- 2、修复相机按键、蓝牙自拍杆监听事件的问题
|
||||
|
||||
> 功能优化
|
||||
|
||||
- 1、相机震动做了兼容处理
|
||||
- 2、相机的权限申请做了兼容处理
|
||||
|
||||
> 计划新增功能(大版本更新【即 `1.2.0` 开始】)
|
||||
|
||||
## 1.1.12(2026-01-19)
|
||||
> 新增功能(临时更新)
|
||||
|
||||
- 无
|
||||
|
||||
> 问题修复
|
||||
|
||||
- 1、修复安卓8.1事打开相机报错:“相机初始失败或者当前设备不支持:创建失败:When targetSdkVersion >= 33
|
||||
should use amdroid.permission.xxx,...”
|
||||
|
||||
> 功能优化
|
||||
|
||||
- 无
|
||||
|
||||
> 计划新增功能(大版本更新【即 `1.2.0` 开始】)
|
||||
## 1.1.11(2026-01-07)
|
||||
|
||||
> 新增功能(临时更新)
|
||||
|
||||
- 1、增加`take-error(相机拍摄监测)`、`orientation-change(相机角度转换)`、、`camera-change(相机设置监听)`
|
||||
|
||||
> 问题修复
|
||||
|
||||
- 1、在`setSizeSelectors`方法增加错误回调,便于排查一些手机调取无反应的问题
|
||||
|
||||
> 功能优化
|
||||
|
||||
- 无
|
||||
|
||||
> 计划新增功能(大版本更新【即 `1.2.0` 开始】)
|
||||
|
||||
## 1.1.10(2026-01-07)
|
||||
|
||||
> 新增功能(临时更新)
|
||||
|
||||
- 1、增加`take-error(相机拍摄监测)`、`orientation-change(相机角度转换)`、、`camera-change(相机设置监听)`
|
||||
|
||||
> 问题修复
|
||||
|
||||
- 1、在`setSizeSelectors`方法增加错误回调,便于排查一些手机调取无反应的问题
|
||||
|
||||
> 功能优化
|
||||
|
||||
- 无
|
||||
|
||||
> 计划新增功能(大版本更新【即 `1.2.0` 开始】)
|
||||
|
||||
## 1.1.9(2026-01-06)
|
||||
|
||||
> 新增功能(临时更新)
|
||||
|
||||
- 1、新增加载相机基础参数方法`loadCameraView`
|
||||
|
||||
> 问题修复
|
||||
|
||||
- 1、修复在 `HBuilderX 4.76` 及以下版本打包(包括自定义基座)时,在kotlin文件中无法获取当前
|
||||
`UTSAndroid.getUniActivity()`、 `UTSAndroid.getAppContext()`的问题
|
||||
- 2、修复在 `HBuilderX 4.76` 及以下版本打包(包括自定义基座)时白屏后闪退的问题
|
||||
- 3、修复高版本`安卓15+`及以上获取的SdkVersion值与当前应用实际的SdkVersion值不一致导致相机获取权限崩溃后闪退的问题
|
||||
|
||||
> 功能优化
|
||||
|
||||
- 无
|
||||
|
||||
> 计划新增功能(大版本更新【即 `1.2.0` 开始】)
|
||||
|
||||
## 1.1.8(2026-01-05)
|
||||
|
||||
> 新增功能(临时更新)
|
||||
|
||||
- 1、是否将拍摄文件保存到本地可见媒体(即相册)的方法`changeGallery`
|
||||
|
||||
> 问题修复
|
||||
|
||||
- 1、相机权限的问题,如用户拒绝后可直接跳转到当前 App 的系统设置页
|
||||
|
||||
> 功能优化
|
||||
|
||||
- 无
|
||||
|
||||
> 计划新增功能(大版本更新【即 `1.2.0` 开始】)
|
||||
|
||||
## 1.1.7(2026-01-04)
|
||||
|
||||
> 新增功能(临时更新)
|
||||
|
||||
- 无
|
||||
|
||||
> 问题修复
|
||||
|
||||
- 1.1.6 版本推包后上传不成功的问题(下载和导入还是1.1.5的代码)
|
||||
|
||||
> 功能优化
|
||||
|
||||
- 无
|
||||
|
||||
> 计划新增功能(大版本更新【即 `1.2.0` 开始】)
|
||||
|
||||
## 1.1.6(2026-01-04)
|
||||
|
||||
> 新增功能(临时更新)
|
||||
|
||||
- 无
|
||||
|
||||
> 问题修复
|
||||
|
||||
- 1、安卓高版本(安卓16)出现拍照的问题
|
||||
- 2、在一些其他终端设备(非手机)中出现相机预览画面与实际不符合的问题`【待复测】`
|
||||
- 3、一些配置不生效的问题,如changeAspectRatio、changeOrientation(由于相机加载顺序问题导致的)
|
||||
- 4、一些其他终端设备(非手机)进入卡顿或者黑屏几秒才显示相机预览页面`【待复测】`
|
||||
- 5、一些手机设备由于权限问题导致第一次进入是黑屏或者后几次进入偶尔出现黑屏闪现的问题`【待复测】`
|
||||
|
||||
> 功能优化
|
||||
|
||||
- 1、将之前的uts改成了kotlin的写法
|
||||
- 2、将组件是的抛出调整成`CameraManager.setCameraCallback`,方便后期增加参数输出、控制
|
||||
- 3、优化了相机加载的顺序问题、以及设置相机时的问题
|
||||
|
||||
> 计划新增功能(大版本更新【即 `1.2.0` 开始】)
|
||||
|
||||
## 1.1.5(2025-12-29)
|
||||
|
||||
> 新增功能(临时更新)
|
||||
|
||||
- 新增设置相机使用设备方向的方法`changeOrientation`
|
||||
- 新增设置相机网格及颜色的方法`changeGrid`
|
||||
|
||||
> 问题修复
|
||||
|
||||
- 无
|
||||
|
||||
> 功能优化
|
||||
|
||||
- 无
|
||||
|
||||
> 计划新增功能(大版本更新【即 `1.2.0` 开始】)
|
||||
|
||||
- 完善鸿蒙`Harmony`版本的并进行更新(已完成开发,测试中)
|
||||
|
||||
## 1.1.4(2025-12-28)
|
||||
|
||||
> 新增功能(临时更新)
|
||||
|
||||
- 新增设置曝光值的方法`changeExposure`
|
||||
|
||||
> 问题修复
|
||||
|
||||
- 修复动态权限申请的问题(即Android 12 及以下、Android 13+)
|
||||
- 修复 Android 16(即API级别为36)时摄像回调的问题
|
||||
|
||||
> 功能优化
|
||||
|
||||
- 无
|
||||
|
||||
> 计划新增功能(大版本更新【即 `1.2.0` 开始】)
|
||||
|
||||
- 完善鸿蒙`Harmony`版本的并进行更新
|
||||
|
||||
## 1.1.3(2025-12-02)
|
||||
|
||||
> 新增功能(临时更新)
|
||||
|
||||
- 无
|
||||
|
||||
> 问题修复
|
||||
|
||||
- 打包报错问题修复,(4.85版本有问题,4.76以下的版本未复现)
|
||||
- 报错内容:
|
||||
`ima-camera-view/utssdk/app-android/src/index.kt:723:55 Argument type mismatch: actual type is 'Any', but 'Number' was expected`
|
||||
|
||||
> 功能优化
|
||||
|
||||
- 无
|
||||
|
||||
> 计划新增功能(大版本更新【即 `1.2.0` 开始】)
|
||||
|
||||
- 完善鸿蒙`Harmony`版本的并进行更新
|
||||
|
||||
## 1.1.2(2025-11-06)
|
||||
|
||||
> 新增功能(临时更新)
|
||||
|
||||
- 无
|
||||
|
||||
> 问题修复
|
||||
|
||||
- `快捷键拍照(蓝牙自拍杆、手机音量键)`拍照时,返回后按钮无法操作的问题修复
|
||||
- 页面在没启用`shortcut(快捷键拍照)`时,也会进入快捷键的问题修复
|
||||
- 修复在低版本`HubuildX`时打包出现找不到类型的问题
|
||||
|
||||
> 功能优化
|
||||
|
||||
- 优化了相机资源在进入时卡顿的问题
|
||||
|
||||
> 计划新增功能(大版本更新【即 `1.2.0` 开始】)
|
||||
|
||||
- 完善鸿蒙`Harmony`版本的并进行更新
|
||||
|
||||
## 1.1.1(2025-11-03)
|
||||
|
||||
> 新增功能(临时更新)
|
||||
|
||||
- 新增设置照片输出格式的方法`changeSuffix`
|
||||
|
||||
> 问题修复
|
||||
|
||||
- 修复照片格式无法设置的问题【原因是在uni给出的生命周期内,无法取到`props`的参数,只能在`watch`中处理】
|
||||
|
||||
> 功能优化
|
||||
|
||||
- 无
|
||||
|
||||
> 计划新增功能(大版本更新【即 `1.2.0` 开始】)
|
||||
|
||||
- 完善鸿蒙`Harmony`版本的并进行更新
|
||||
|
||||
## 1.1.0(2025-10-31)
|
||||
|
||||
> 新增功能
|
||||
|
||||
- 快捷键拍照: 如:按两下音量键拍照、按音量键拍照等等(可自定义)
|
||||
- 蓝牙自拍杆: 提供可以连接蓝牙自拍杆拍照、对焦等(可自定义)
|
||||
- 相机`是否开启蓝牙自拍杆、手机快捷键拍照`,可自定义快捷键,具体参考参数`shortcut`
|
||||
-
|
||||
|
||||
> 问题修复
|
||||
|
||||
- 无
|
||||
|
||||
> 功能优化
|
||||
|
||||
- 无
|
||||
|
||||
> 计划新增功能(大版本更新【即 `1.2.0` 开始】)
|
||||
|
||||
- 完善鸿蒙`Harmony`版本的并进行更新
|
||||
|
||||
## 1.0.5(2025-10-30)
|
||||
|
||||
> 新增功能(临时更新)
|
||||
|
||||
- 相机`录像`方法增加设置`视频录制时长限制`
|
||||
- 相机`录像`声音默认系统录像声音,也可自定义,具体参考参数`recorder`、`sound2`
|
||||
|
||||
> 问题修复
|
||||
|
||||
- 无
|
||||
|
||||
> 功能优化
|
||||
|
||||
- 无
|
||||
|
||||
## 1.0.4(2025-10-18)
|
||||
|
||||
> 新增功能(临时更新)
|
||||
|
||||
- 增加设置相机`白平衡`方法:`changeWhiteBalance`
|
||||
- 增加设置相机`HDR`方法:`changeHdr`
|
||||
- 增加设置相机`特定比例`方法:`changeAspectRatio`
|
||||
- 新增了`widthRatio`、`heightRatio`、`tolerance`、`whiteBalance`、`hdr`、`shutter`、`sound`、`vibrate`、
|
||||
`duration`等参数,具体参考API
|
||||
|
||||
> 问题修复
|
||||
|
||||
- 无
|
||||
|
||||
> 功能优化
|
||||
|
||||
- 照片文件的尺寸和手机原相机的尺寸不对的问题,可以通过参数`duration`去控制,其中`全屏`、`1:1`
|
||||
基本是和原相机尺寸一致的(测试了大部分手机型号都OK,个别手机`3:4`、`4:3`、`9:16`时会有少许容差,可以通过参数
|
||||
`duration`去控制)
|
||||
|
||||
## 1.0.3(2025-10-16)
|
||||
|
||||
> 新增功能
|
||||
|
||||
- 增加拍照声音,可以自定义声音文件(默认手机原相机声音)
|
||||
- 增加拍照震动,可以自定义震动时长(默认200毫秒)
|
||||
|
||||
> 问题修复
|
||||
> 无
|
||||
|
||||
> 功能优化
|
||||
|
||||
- 照片文件的分辨率取手机原相机的,提高照片的清晰度
|
||||
|
||||
## 1.0.2(2025-09-09)
|
||||
|
||||
> 新增功能
|
||||
> 无
|
||||
|
||||
> 问题修复
|
||||
> 无
|
||||
|
||||
> 功能优化
|
||||
|
||||
- 支持拍照预览或从相册选择后,返回当前相机页面时,如果出现黑屏状态,可以重新自己手动拉起相机
|
||||
|
||||
## 1.0.1(2025-09-05)
|
||||
|
||||
> 新增功能
|
||||
> 无
|
||||
|
||||
> 问题修复
|
||||
|
||||
- 修复高版本(`4.66`时)打包报错:`*(项目路径)*//index.kt:44:12 Unresolved reference: _uA`、
|
||||
`*(项目路径)*/index.kt:118:48 Unresolved reference: _uO`的问题
|
||||
- 修复前置拍出来的图片是镜像的问题
|
||||
- 修复照片拍出来尺寸不对、照片过小的问题
|
||||
|
||||
> 功能优化
|
||||
> 无
|
||||
|
||||
## 1.0.0(2025-05-27)
|
||||
|
||||
> 新增功能
|
||||
|
||||
- 新增方法:`close`、 `open`、 `takePhoto`、 `takePhotoSnapshot`、 `takeVideo`、`takeVideoSnapshot`、
|
||||
`stopVideo`、 `changeZoom`、 `changeFacing`、`changeFlash`、`changeAudio`
|
||||
- 新增事件:`onPictureTaken`、 `onVideoTakenStart`、 `onVideoTakenEnd`、 `onFocusStart`、 `onFocusEnd`
|
||||
- `android端`的所有功能已完成开发、测试
|
||||
- `harmony端`计划开发中
|
||||
- 初始版
|
||||
|
||||
> 问题修复
|
||||
> 无
|
||||
|
||||
> 功能优化
|
||||
> 无
|
||||
BIN
uni_modules/ima-camera-view/encrypt
Normal file
BIN
uni_modules/ima-camera-view/encrypt
Normal file
Binary file not shown.
126
uni_modules/ima-camera-view/package.json
Normal file
126
uni_modules/ima-camera-view/package.json
Normal file
@ -0,0 +1,126 @@
|
||||
{
|
||||
"id": "ima-camera-view",
|
||||
"displayName": "原生Camera自定义相机拍照、视频录制",
|
||||
"version": "1.2.2",
|
||||
"description": "原生Camera相机开发的UTS插件,支持相机拍照、视频录制、可实现点击聚焦、手势缩放、自定义布局、自定义蒙版(用于人脸拍照,身份证拍照等),同时支持蓝牙自拍杆(可自定义)、手机快捷键(可自定义)",
|
||||
"keywords": [
|
||||
"原生Camera",
|
||||
"相机拍照",
|
||||
"视频录制",
|
||||
"自定义布局相机",
|
||||
"自定义蒙版相机"
|
||||
],
|
||||
"repository": "",
|
||||
"engines": {
|
||||
"HBuilderX": "^4.8.1",
|
||||
"uni-app": "^4.81",
|
||||
"uni-app-x": "^4.81"
|
||||
},
|
||||
"dcloudext": {
|
||||
"type": "component-uts",
|
||||
"sale": {
|
||||
"regular": {
|
||||
"price": "19.99"
|
||||
},
|
||||
"sourcecode": {
|
||||
"price": "299.00"
|
||||
}
|
||||
},
|
||||
"contact": {
|
||||
"qq": "488266488"
|
||||
},
|
||||
"declaration": {
|
||||
"ads": "无",
|
||||
"data": "插件不采集任何数据",
|
||||
"permissions": "摄像头、音频、文件读取、文件写入\n <uses-permission android:name=\"android.permission.CAMERA\" />\n <uses-permission android:name=\"android.permission.RECORD_AUDIO\" />\n <uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\" />\n <uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\" />\n <uses-permission android:name=\"android.permission.VIBRATE\" />"
|
||||
},
|
||||
"npmurl": "",
|
||||
"darkmode": "x",
|
||||
"i18n": "x",
|
||||
"widescreen": "√"
|
||||
},
|
||||
"uni_modules": {
|
||||
"dependencies": [],
|
||||
"encrypt": [],
|
||||
"platforms": {
|
||||
"cloud": {
|
||||
"tcb": "x",
|
||||
"aliyun": "x",
|
||||
"alipay": "x"
|
||||
},
|
||||
"client": {
|
||||
"uni-app": {
|
||||
"vue": {
|
||||
"vue2": {
|
||||
"extVersion": "1.1.13",
|
||||
"minVersion": ""
|
||||
},
|
||||
"vue3": {
|
||||
"extVersion": "1.1.13",
|
||||
"minVersion": ""
|
||||
}
|
||||
},
|
||||
"web": {
|
||||
"safari": "x",
|
||||
"chrome": "x"
|
||||
},
|
||||
"app": {
|
||||
"vue": {
|
||||
"extVersion": "1.1.13",
|
||||
"minVersion": ""
|
||||
},
|
||||
"nvue": {
|
||||
"extVersion": "1.1.13",
|
||||
"minVersion": ""
|
||||
},
|
||||
"android": {
|
||||
"extVersion": "1.1.13",
|
||||
"minVersion": "21"
|
||||
},
|
||||
"ios": "x",
|
||||
"harmony": {
|
||||
"extVersion": "1.1.13",
|
||||
"minVersion": "5.0以下(不包括5.0)"
|
||||
}
|
||||
},
|
||||
"mp": {
|
||||
"weixin": "x",
|
||||
"alipay": "x",
|
||||
"toutiao": "x",
|
||||
"baidu": "x",
|
||||
"kuaishou": "x",
|
||||
"jd": "x",
|
||||
"harmony": "x",
|
||||
"qq": "x",
|
||||
"lark": "x",
|
||||
"xhs": "-"
|
||||
},
|
||||
"quickapp": {
|
||||
"huawei": "x",
|
||||
"union": "x"
|
||||
}
|
||||
},
|
||||
"uni-app-x": {
|
||||
"web": {
|
||||
"safari": "x",
|
||||
"chrome": "x"
|
||||
},
|
||||
"app": {
|
||||
"android": {
|
||||
"extVersion": "1.1.13",
|
||||
"minVersion": "21"
|
||||
},
|
||||
"ios": "x",
|
||||
"harmony": {
|
||||
"extVersion": "1.1.13",
|
||||
"minVersion": "5.0以下(不包括5.0)"
|
||||
}
|
||||
},
|
||||
"mp": {
|
||||
"weixin": "x"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
373
uni_modules/ima-camera-view/readme.md
Normal file
373
uni_modules/ima-camera-view/readme.md
Normal file
@ -0,0 +1,373 @@
|
||||
# 原生自定义相机拍照、视频录制 (ima-camera-view)
|
||||
|
||||
`原生自定义相机拍照、视频录制 (ima-camera-view)`是基于原生相机开发的UTS插件,支持`相机拍照`、`视频录制`
|
||||
、可实现`点击聚焦`、`手势缩放`、
|
||||
`自定义布局`、`自定义蒙版`(用于人脸拍照,身份证拍照等)。
|
||||
|
||||
## ⚠️注意️
|
||||
由于之前技术选型不支持继续扩展相机的新特性,`本插件(ima-camera-view)继续维护,只修复现存的一些bug,不再新增新功能`,
|
||||
建议使用ima-camerax-view这个相机组件插件,[ima-camerax-view 地址](https://ext.dcloud.net.cn/plugin?name=ima-camerax-view),
|
||||
如果不追求如:**超广角镜头切换(0.5x,`changeWideAngle`)**、**按倍率缩放(`setCameraZoom`,如 0.5 / 1.0 / 2.0)**、**多摄逻辑镜头切换**、
|
||||
**Camera2 白平衡 / HDR / 曝光补偿**、**录像进度(`onVideoTakenProgress`)与设备方向变化(`onOrientationChanged`)回调**等新特性的话,扔建议继续使用本插件。
|
||||
|
||||
- `ima-camerax-view【新插件】`:[插件地址](https://ext.dcloud.net.cn/plugin?name=ima-camerax-view)
|
||||
- `ima-camera-view【本插件】`:[插件地址](https://ext.dcloud.net.cn/plugin?name=ima-camera-view)
|
||||
- `ima-camerax-view【新插件】`与 `ima-camera-view【本插件】`组件 API 保持基本一致,便于从 CameraView 版平滑迁移
|
||||
|
||||
## 支持功能
|
||||
|
||||
- 打开、关闭摄像头预览
|
||||
- 拍照、快照拍照
|
||||
- 录制视频、快照录制视频
|
||||
- 设置摄像头缩放级别
|
||||
- 设置相机白平衡
|
||||
- 设置相机HDR
|
||||
- 设置相机曝光
|
||||
- 设置摄像头方向
|
||||
- 设置闪光灯模式
|
||||
- 设置相机使用设备方向
|
||||
- 设置相机网格及颜色
|
||||
- 设置音频(录制视频时)
|
||||
- 设置圆角、圆预览(可自定义)
|
||||
- 外接摄像头预览方向修正(`previewRotation`,Android)
|
||||
- 设置拍照、录制视频的声音(可自定义)
|
||||
- 蓝牙自拍杆(可自定义)
|
||||
- 手机快捷键(可自定义)
|
||||
|
||||
## 自定义调整
|
||||
|
||||
- 自定义调整页面地址:`uni_modules/ima-camera-view/utssdk/app-android/index.vue`
|
||||
- 蓝牙自拍杆、手机快捷键的自定义,可以参考文件中的`shortcutListener`方法
|
||||
- 设置拍照、录制视频的声音,可以参考文件中的`photoSound`、`videoSound`方法
|
||||
|
||||
## 需要权限
|
||||
|
||||
- 摄像头、音频、文件读取、文件写入、震动
|
||||
|
||||
```text
|
||||
"android.permission.CAMERA",
|
||||
"android.permission.RECORD_AUDIO",
|
||||
"android.permission.VIBRATE"
|
||||
"android.permission.READ_EXTERNAL_STORAGE"
|
||||
"android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
```
|
||||
|
||||
- 即:在`manifest.json`中的`distribute.android.permissions`加入
|
||||
|
||||
```text
|
||||
// 拍摄照片和视频时需要
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
// 拍摄视频时需要Audio.ON(默认)
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
// 读取拍照、录像文件文件时需要
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
// 报错拍照、录像文件文件时需要(默认保存到沙盒缓存)
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
// 震动权限
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
```
|
||||
|
||||
## 快门声音素材
|
||||
|
||||
- 将需要的快门声音放在`uni_modules/ima-camera-view/utssdk/app-android/assets`下即可
|
||||
- [熊猫办公](https://www.tukuppt.com/yinxiaomuban/kuaimenshengyin.html)
|
||||
- [站长素材](https://sc.chinaz.com/tag_yinxiao/kuaimen.html)
|
||||
|
||||
## 使用示例【此示例的代码只实现了`相机拍照`的逻辑,更多示例请导入项目】
|
||||
|
||||
- 新建一个`camera.nvue`的文件
|
||||
- ⚠️注意️:只能在`.nvue`、`.uvue`的文件后缀下才生效,不支持`.vue`
|
||||
|
||||
```nvue
|
||||
<template>
|
||||
<view class="ima-camera" :style="{ width: windowWidth, height: windowHeight }">
|
||||
<ima-camera-view
|
||||
ref="cameraRef"
|
||||
class="camera-view"
|
||||
:style="{ width: windowWidth + 'px', height: windowHeight + 'px' }"
|
||||
flash="on"
|
||||
@onPictureTaken="onPictureTaken"
|
||||
onFocusStart="onFocusStart"
|
||||
/>
|
||||
<view class="camera-menu">
|
||||
<!--返回键-->
|
||||
<cover-image @tap="back" class="camera-menu-button back" src="/static/camera/back.png" />
|
||||
<!--快门键-->
|
||||
<cover-image
|
||||
@tap="takePhoto"
|
||||
class="camera-menu-button shutter"
|
||||
src="/static/camera/shutter.png"
|
||||
/>
|
||||
<!--反转键-->
|
||||
<cover-image @tap="flip" class="camera-menu-button flip" src="/static/camera/flip.png" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
let _this = null
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
windowWidth: '', //屏幕可用宽度
|
||||
windowHeight: '', //屏幕可用高度
|
||||
facing: 'back'
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
_this = this
|
||||
this.initCamera()
|
||||
},
|
||||
methods: {
|
||||
//初始化相机
|
||||
initCamera() {
|
||||
console.log('初始化相机')
|
||||
uni.getSystemInfo({
|
||||
success: (res) => {
|
||||
_this.windowWidth = res.windowWidth
|
||||
_this.windowHeight = res.windowHeight
|
||||
}
|
||||
})
|
||||
},
|
||||
onFocusStart(e) {
|
||||
console.log('聚焦', e)
|
||||
},
|
||||
takePhoto() {
|
||||
console.log('拍照', this.facing)
|
||||
this.$refs.cameraRef.takePhoto()
|
||||
},
|
||||
//返回
|
||||
back() {
|
||||
console.log('返回上一页', this.facing)
|
||||
uni.navigateBack()
|
||||
},
|
||||
//反转
|
||||
flip() {
|
||||
console.log('镜头反转', this.facing)
|
||||
this.facing = this.facing === 'back' ? 'front' : 'back'
|
||||
this.$refs.cameraRef.changeFacing(this.facing)
|
||||
},
|
||||
onPictureTaken(e) {
|
||||
console.log('拍照结果', e.detail)
|
||||
_this.snapshotsrc = e.detail?.path || ''
|
||||
_this.getTakenRes()
|
||||
uni.navigateBack()
|
||||
},
|
||||
//设置
|
||||
getTakenRes() {
|
||||
console.log('返回结果给上一页')
|
||||
let pages = getCurrentPages()
|
||||
let prevPage = pages[pages.length - 2] //上一个页面
|
||||
//直接调用上一个页面的setImage()方法,把数据存到上一个页面中去
|
||||
prevPage.$vm.setImage({ path: _this.snapshotsrc })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ima-camera {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.camera-view {
|
||||
width: 100%;
|
||||
background: #111;
|
||||
}
|
||||
|
||||
.camera-menu {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 750rpx;
|
||||
height: 180rpx;
|
||||
z-index: 98;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&-button {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
position: absolute;
|
||||
bottom: 50rpx;
|
||||
z-index: 99;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.back {
|
||||
left: 30rpx;
|
||||
}
|
||||
|
||||
.shutter {
|
||||
width: 130rpx;
|
||||
height: 130rpx;
|
||||
left: 310rpx;
|
||||
bottom: 25rpx;
|
||||
}
|
||||
|
||||
.flip {
|
||||
right: 30rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
## 外接摄像头方向修正(Android)
|
||||
|
||||
适用于**无内置摄像头**或外接 USB 摄像头的设备(如横屏平板、工控一体机)。当物理安装的摄像头与屏幕方向不一致,导致预览画面旋转 90° 等情况时,可通过 `previewRotation` 固定修正预览与拍照方向。
|
||||
|
||||
### 使用方式
|
||||
|
||||
**方式一:属性(推荐)**
|
||||
|
||||
```nvue
|
||||
<ima-camera-view
|
||||
ref="cameraRef"
|
||||
:previewRotation="90"
|
||||
:previewCornerRadiusRate="0.5"
|
||||
facing="front"
|
||||
/>
|
||||
```
|
||||
|
||||
**方式二:方法动态调整**
|
||||
|
||||
```javascript
|
||||
this.$refs.cameraRef.changePreviewRotation(90) // 顺时针 90°
|
||||
this.$refs.cameraRef.changePreviewRotation(-90) // 逆时针 90°(等价 270°)
|
||||
```
|
||||
|
||||
### 参数说明
|
||||
|
||||
| 取值 | 说明 |
|
||||
|------|------|
|
||||
| `0` | 不修正(默认) |
|
||||
| `90` / `-270` | 顺时针旋转 90° |
|
||||
| `180` / `-180` | 旋转 180° |
|
||||
| `270` / `-90` | 顺时针旋转 270°(逆时针 90°) |
|
||||
|
||||
### 注意事项
|
||||
|
||||
- 仅 **`android`** 端生效;与 `orientation`(设备方向跟随)不同,`previewRotation` 用于**固定补偿外接摄像头安装角度**,不随手机旋转而改变。
|
||||
- 启用非 `0` 的 `previewRotation` 后,组件会自动使用 `TextureView` 预览;拍照结果会同步叠加该角度修正。
|
||||
- 若预览方向仍不对,请依次尝试 `90`、`-90`、`180`,以实际安装方向为准。
|
||||
- 录像方向暂未做同等叠加修正;若录像也有方向问题,可在业务层二次处理或反馈 issue。
|
||||
|
||||
## 常见的比例的定义(widthRatio,heightRatio)
|
||||
|
||||
```typescript
|
||||
// 正方形
|
||||
AspectRatio.of(1, 1) // 1:1
|
||||
|
||||
// 竖屏比例
|
||||
AspectRatio.of(9, 16) // 9:16 (手机竖屏)
|
||||
AspectRatio.of(3, 4) // 3:4
|
||||
AspectRatio.of(2, 3) // 2:3
|
||||
AspectRatio.of(10, 16) // 10:16 (5:8)
|
||||
|
||||
// 横屏比例
|
||||
AspectRatio.of(16, 9) // 16:9 (宽屏)
|
||||
AspectRatio.of(4, 3) // 4:3 (传统)
|
||||
AspectRatio.of(3, 2) // 3:2 (照片)
|
||||
AspectRatio.of(16, 10) // 16:10 (8:5)
|
||||
AspectRatio.of(21, 9) // 21:9 (超宽屏)
|
||||
|
||||
// 建议比例
|
||||
const AspectRatios = {
|
||||
// 1:1 正方形
|
||||
SQUARE: AspectRatio.of(1, 1),
|
||||
// 9:16 竖屏(手机默认)
|
||||
PORTRAIT: AspectRatio.of(9, 16),
|
||||
// 16:9 横屏
|
||||
LANDSCAPE: AspectRatio.of(16, 9),
|
||||
// 3:4 传统照片比例
|
||||
THREE_FOUR: AspectRatio.of(3, 4),
|
||||
// 4:3 传统相机比例
|
||||
FOUR_THREE: AspectRatio.of(4, 3)
|
||||
}
|
||||
```
|
||||
|
||||
## 不同场景的推荐值(tolerance)
|
||||
|
||||
```typescript
|
||||
const TOLERANCE = {
|
||||
STRICT: 0.01.toFloat(), // 非常严格,几乎精确匹配
|
||||
STANDARD: 0.05.toFloat(), // 标准,推荐使用
|
||||
FLEXIBLE: 0.1.toFloat(), // 灵活,兼容更多设备
|
||||
LOOSE: 0.2.toFloat() // 宽松,可能匹配到意外比例
|
||||
}
|
||||
```
|
||||
|
||||
## Api
|
||||
|
||||
| 属性 | 类型 | 默认值 | 说明 | 平台 |
|
||||
|---------------|-----------------|-------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------|
|
||||
| widthRatio | Number | 0 | 照片尺寸比率(宽度): 默认全屏(若widthRatio为9,heightRatio为16,则为9:16、widthRatio为3,heightRatio为4,则为3:4...)不建议值过大,常用的比例有1:1、3:4、4:3、9:16... | `android` |
|
||||
| heightRatio | Number | 0 | 照片尺寸比率(高度): 默认全屏(若widthRatio为9,heightRatio为16,则为9:16、widthRatio为3,heightRatio为4,则为3:4...)不建议值过大,常用的比例有1:1、3:4、4:3、9:16... | `android` |
|
||||
| tolerance | Number | 0.1 | 照片尺寸容差值: 建议设为 0.05~0.15,以便稍微兼容不同设备相机实际比例差异 ,值为:0~1 | `android` |
|
||||
| whiteBalance | iWhiteBalance | "auto" | 白平衡模式: auto(自动)、incandescent(白炽)、fluorescent(荧光)、daylight(日光)、cloudy(多云) 、loudy(多云【慎用:`兼容1.1.3版本前单词拼错的问题,将在1.2.0大版本更新后删除,建议1.1.3后的版本使用cloudy`】) | `android` |
|
||||
| hdr | iHdr | "off" | HDR模式: off(关闭)、on(开启) | `android` |
|
||||
| facing | iFacing | "back" | 后置、前置摄像头: back(后置摄像头)、front(前置摄像头) | `android` |
|
||||
| flash | iFlash | "off" | 闪光灯: off(关闭)、on(开启)、auto(自动)、torch(常开) | `android` |
|
||||
| audio | iAudio | "on" | 音频: on(开启)、off(关闭)、mono(单声道)、stereo(立体声) | `android` |
|
||||
| orientation | iOrientation | "auto" | 方向: auto(自动)、portrait(竖屏)、landscape(横屏) 【注意:`目前竖屏、横屏拍出来都为竖屏方式,这两个参数的使用效果一致`,为预留参数,为后期做准备】 | `android` |
|
||||
| grid | iGrid | "off" | 网格: off(关闭)、draw_3X3(3x3)、draw_4x4(4x4)、draw_phi(phi) | `android` |
|
||||
| gridColor | String | "#808080" | 颜色值: 只支持如:#fff、#ffffff、#......等类型的颜色值 | `android` |
|
||||
| photoSuffix | iPhotoSuffix | "jpeg" | 照片格式: jpeg、jpg | `android` |
|
||||
| mode | String | 'picture' | 相机模式: picture(拍照)、video(录视频) | `android` |
|
||||
| previewCornerRadius | Number | 0 | 预览圆角半径(px);与 `previewCornerRadiusRate` 二选一或组合使用 | `android` |
|
||||
| previewCornerRadiusRate | Number | 0 | 预览圆角比例(相对短边);`0.5` 且预览区域为正方形时为圆形预览 | `android` |
|
||||
| previewRotation | Number | 0 | 外接摄像头预览/拍照方向修正角度:`0`、`90`、`180`、`270`(支持负值如 `-90`);详见上文「外接摄像头方向修正」 | `android` |
|
||||
| gallery | Boolean | false | 是否将拍照、录像文件保存到系统相册(可见媒体库) | `android` |
|
||||
| shutter | Boolean | true | 是否打开拍照声音: true(开启,此时配置`sound`才起作用)、false(关闭) | `android` |
|
||||
| sound | String | '' | 相机拍照声音文件: 将mp3音频文件放在`uni_modules/ima-camera-view/utssdk/app-android/assets`下即可,为音频文件的名称,如`xxx.mp3`,默认手机原声 | `android` |
|
||||
| recorder | Boolean | true | 是否打开录像声音: true(开启,此时配置`sound2`才起作用)、false(关闭) | `android` |
|
||||
| sound2 | String | '' | 相机录像声音文件: 将mp3音频文件放在`uni_modules/ima-camera-view/utssdk/app-android/assets`下即可,为音频文件的名称,如`xxx.mp3`,默认手机原声 | `android` |
|
||||
| vibrate | Boolean | false | 是否打开拍照震动: true(开启,此时配置`duration`才起作用)、false(关闭) | `android` |
|
||||
| duration | Number | 300 | 是否打开拍照震动时长,单位:毫秒(ms) | `android` |
|
||||
| shortcut | Boolean | false | 是否开启蓝牙自拍杆、手机快捷键拍照: false(关闭)、true(开启) | `android` |
|
||||
|
||||
## 方法
|
||||
|
||||
### 共同 方法/* */
|
||||
|
||||
| 方法名称 | 说明 | 方法参数 | 平台 |
|
||||
|-------------------------------------------------------|---------------------------|-----------------------------------------------------------------------------------------------------------------------|-------------|
|
||||
| open | 打开摄像头预览 | 无 | `android` |
|
||||
| close | 关闭摄像头预览 | 无 | `android` |
|
||||
| takePhoto | 拍照(标准拍照流程) | 无 | `android` |
|
||||
| takeVideo(duration) | 开始录制视频 | duration:拍摄时长,单位:毫秒(ms)【0 表示不限制】 | `android` |
|
||||
| stopVideo | 停止视频录制 | 无 | `android` |
|
||||
| changeZoom(zoom) | 设置摄像头缩放级别 | zoom:缩放倍数(浮点数) | `android` |
|
||||
| changeExposure(exposure) | 设置曝光值 | exposure:曝光值(浮点数)【值为:-2~2,默认为0】 | `android` |
|
||||
| changeWhiteBalance(whiteBalance) | 设置相机白平衡 | whiteBalance:参考`api`中的`whiteBalance`参数 | `android` |
|
||||
| changeHdr(hdr) | 设置相机HDR | hdr:参考`api`中的`hdr`参数 | `android` |
|
||||
| changeFacing((facing)) | 设置摄像头方向 | facing:参考`api`中的`facing`参数 | `android` |
|
||||
| changeFlash(flash) | 设置闪光灯模式 | flash:参考`api`中的`flash`s参数 | `android` |
|
||||
| changeOrientation(orientation) | 设置相机使用设备方向 | orientation:参考`api`中的`orientation`s参数 | `android` |
|
||||
| changeGrid(grid,color) | 设置相机网格及颜色 | grid:参考`api`中的`grid`参数, color: 参考`api`中的`gridColor`参数 | `android` |
|
||||
| changeAudio(audio) | 设置音频 | audio:参考`api`中的`audio`参数 | `android` |
|
||||
| changeSuffix(suffix) | 设置照片输出格式 | suffix:参考`api`中的`photoSuffix`参数 | `android` |
|
||||
| changeSizeSelectors(width,height,tolerance) | 设置相机特定比例/分辨率 | width、height、tolerance 参考 `api` 中同名属性;`0` 表示使用屏幕宽高 | `android` |
|
||||
| changePreviewCorner(radius,radiusRate) | 设置预览圆角 | radius: 圆角 px;radiusRate: 圆角比例(正方形下 `0.5` 为圆形) | `android` |
|
||||
| changePreviewRotation(degrees) | 设置外接摄像头预览/拍照方向修正 | degrees: `0` / `90` / `180` / `270`(支持负值);仅 Android | `android` |
|
||||
| changeGallery(gallery) | 是否保存到系统相册 | gallery: `true` / `false` | `android` |
|
||||
| changeMode(mode) | 设置相机模式 | mode: `picture` / `video`;也可直接使用 `:mode` prop | `android` |
|
||||
| takePhotoSnapshot | 快照拍照(适用于快速拍照场景) | 无 | `android` |
|
||||
| takeVideoSnapshot(duration) | 快照方式录制视频 | duration:拍摄时长,单位:毫秒(ms)【0 表示不限制】 | `android` |
|
||||
|
||||
## 事件
|
||||
|
||||
| 事件名称 | 说明 | 回调参数 | 平台 |
|
||||
|---------------------|---------------|-----------------------------------|-------------|
|
||||
| onPictureTaken | 拍照返回数据 | ({path,width,height}: any) => {} | `android` |
|
||||
| onVideoTakenStart | 录制视频开始事件 | () => {} | `android` |
|
||||
| onVideoTakenEnd | 录制视频结束事件 | ({path,size}: any) => {} | `android` |
|
||||
| onFocusStart | 自动对焦开始 | ({x,y}: any) => {} | `android` |
|
||||
| onFocusEnd | 自动对焦结束 | ({x,y,focus}: any) => {} | `android` |
|
||||
| onOrientationChange | 设备方向变化 | ({angle,orientation,isPortrait,isLandscape}: any) => {} | `android` |
|
||||
| onCameraOpened | 相机已打开 | (data: any) => {} | `android` |
|
||||
| onCameraClosed | 相机已关闭 | (data: any) => {} | `android` |
|
||||
| onCameraError | 相机错误 | ({errorCode,reason,message}: any) => {} | `android` |
|
||||
|
||||
@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"
|
||||
package="uts.sdk.modules.imaCameraView">
|
||||
<!-- 相机权限 -->
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<!-- 震动权限 -->
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<!-- 录音权限 -->
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
|
||||
<!-- Android 13+ -->
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
||||
|
||||
<!-- Android 12 及以下 -->
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
|
||||
<!-- 功能声明 -->
|
||||
<uses-feature android:name="android.hardware.camera" android:required="true"/>
|
||||
<uses-feature android:name="android.hardware.camera.autofocus" />
|
||||
<uses-feature android:name="android.hardware.camera.flash" />
|
||||
</manifest>
|
||||
BIN
uni_modules/ima-camera-view/utssdk/app-android/ImaCamera.kt
Normal file
BIN
uni_modules/ima-camera-view/utssdk/app-android/ImaCamera.kt
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
uni_modules/ima-camera-view/utssdk/app-android/ImaCameraUtils.kt
Normal file
BIN
uni_modules/ima-camera-view/utssdk/app-android/ImaCameraUtils.kt
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,9 @@
|
||||
{
|
||||
"minSdkVersion": 21,
|
||||
"dependencies": [
|
||||
{
|
||||
"id": "com.otaliastudios:cameraview",
|
||||
"source": "implementation('com.otaliastudios:cameraview:2.7.2') { exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-jdk7'; exclude group: 'androidx.core', module: 'core' }"
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
uni_modules/ima-camera-view/utssdk/app-android/index.uts
Normal file
BIN
uni_modules/ima-camera-view/utssdk/app-android/index.uts
Normal file
Binary file not shown.
651
uni_modules/ima-camera-view/utssdk/app-android/index.vue
Normal file
651
uni_modules/ima-camera-view/utssdk/app-android/index.vue
Normal file
@ -0,0 +1,651 @@
|
||||
<template>
|
||||
<view>
|
||||
<slot />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
import FrameLayout from 'android.widget.FrameLayout';
|
||||
import { ListenerOptions } from 'com.maxiaoqu.camera'
|
||||
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(): FrameLayout{
|
||||
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 : ListenerOptions = {
|
||||
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,必须定义返回值类型
|
||||
* 开发者需要重点实现这个函数,声明原生组件被创建出来的过程,以及最终生成的原生组件类型
|
||||
* (Android需要明确知道View类型,需特殊校验)
|
||||
*/
|
||||
NVLoad() : FrameLayout {
|
||||
// 必须在 initCameraView 之前设置,确保原生层创建 CameraView 时
|
||||
// previewRotationDegrees 已为非零值,从而在初始化阶段启用 TextureView 预览模式,
|
||||
// 避免后续 setPreview(Texture) 触发相机重启导致旋转丢失
|
||||
setPreviewRotation(this.previewRotation)
|
||||
return this.initCameraView()
|
||||
},
|
||||
/**
|
||||
* [可选实现] 原生View已创建
|
||||
*/
|
||||
NVLoaded() {
|
||||
// 监听页面暂停事件:自动关闭摄像头
|
||||
UTSAndroid.onAppActivityPause(() => {
|
||||
console.log('自动关闭摄像头')
|
||||
if (this.isOpened() == true) {
|
||||
this.close();
|
||||
}
|
||||
});
|
||||
// 监听页面恢复事件:自动打开摄像头
|
||||
UTSAndroid.onAppActivityResume(() => {
|
||||
console.log('自动打开摄像头')
|
||||
if (this.isOpened() != true) {
|
||||
this.open();
|
||||
}
|
||||
});
|
||||
},
|
||||
/**
|
||||
* [可选实现] 原生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();
|
||||
}
|
||||
UTSAndroid.offAppActivityPause();
|
||||
UTSAndroid.offAppActivityResume();
|
||||
},
|
||||
/**
|
||||
* [可选实现] 自定组件布局尺寸,用于告诉排版系统,组件自身需要的宽高
|
||||
* 一般情况下,组件的宽高应该是由终端系统的排版引擎决定,组件开发者不需要实现此函数
|
||||
* 但是部分场景下,组件开发者需要自己维护宽高,则需要开发者重写此函数
|
||||
*/
|
||||
NVMeasure(size : UTSSize) : UTSSize {
|
||||
return size;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Binary file not shown.
14
uni_modules/ima-camera-view/utssdk/app-ios/Info.plist
Normal file
14
uni_modules/ima-camera-view/utssdk/app-ios/Info.plist
Normal file
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>需要您的同意才能使用摄像头进行拍照和录像</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>需要您的同意才能录制视频中的声音</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>需要您的同意才能将拍摄的照片和视频保存到相册</string>
|
||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||
<string>需要您的同意才能将拍摄的照片和视频保存到相册</string>
|
||||
</dict>
|
||||
</plist>
|
||||
10
uni_modules/ima-camera-view/utssdk/app-ios/config.json
Normal file
10
uni_modules/ima-camera-view/utssdk/app-ios/config.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"deploymentTarget": "12.0",
|
||||
"frameworks": [
|
||||
"AVFoundation",
|
||||
"Photos",
|
||||
"UIKit",
|
||||
"Foundation",
|
||||
"AudioToolbox"
|
||||
]
|
||||
}
|
||||
BIN
uni_modules/ima-camera-view/utssdk/app-ios/index.uts
Normal file
BIN
uni_modules/ima-camera-view/utssdk/app-ios/index.uts
Normal file
Binary file not shown.
634
uni_modules/ima-camera-view/utssdk/app-ios/index.vue
Normal file
634
uni_modules/ima-camera-view/utssdk/app-ios/index.vue
Normal file
@ -0,0 +1,634 @@
|
||||
<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>
|
||||
15
uni_modules/ima-camera-view/utssdk/interface.uts
Normal file
15
uni_modules/ima-camera-view/utssdk/interface.uts
Normal file
@ -0,0 +1,15 @@
|
||||
export type iWhiteBalance = 'auto' | 'incandescent' | 'fluorescent' | 'daylight' | 'cloudy' | 'loudy'
|
||||
|
||||
export type iHdr = 'on' | 'off'
|
||||
|
||||
export type iFacing = 'front' | 'back'
|
||||
|
||||
export type iOrientation = 'auto' | 'portrait' | 'landscape'
|
||||
|
||||
export type iGrid = 'off' | 'draw_3x3' | 'draw_4x4' | 'draw_phi'
|
||||
|
||||
export type iFlash = 'on' | 'auto' | 'torch' | 'off'
|
||||
|
||||
export type iAudio = 'on' | 'off' | 'mono' | 'stereo'
|
||||
|
||||
export type iPhotoSuffix = 'jpeg' | 'jpg'
|
||||
20
uni_modules/lime-camera/changelog.md
Normal file
20
uni_modules/lime-camera/changelog.md
Normal file
@ -0,0 +1,20 @@
|
||||
## 0.1.0(2025-09-11)
|
||||
- fix: 修复uniappx nvue无法创建context的问题
|
||||
## 0.0.9(2025-09-11)
|
||||
- feat: uniapp nvue
|
||||
## 0.0.8(2025-05-25)
|
||||
- fix: 修复uniapp 安卓转js问题
|
||||
## 0.0.7(2025-01-11)
|
||||
- feat: 优化报错
|
||||
## 0.0.6(2024-12-05)
|
||||
- fix: 修复变量重复
|
||||
## 0.0.5(2024-12-04)
|
||||
- fix: UniPointerEvent改为UTSJSONObejct
|
||||
## 0.0.4(2024-11-27)
|
||||
- fix: 修复录像BUG
|
||||
## 0.0.3(2024-04-21)
|
||||
- feat: 增加点击对焦
|
||||
## 0.0.2(2024-03-29)
|
||||
- fix: 修复默认值是前置的情况无效
|
||||
## 0.0.1(2024-03-17)
|
||||
- init
|
||||
101
uni_modules/lime-camera/components/lime-camera/lime-camera.uvue
Normal file
101
uni_modules/lime-camera/components/lime-camera/lime-camera.uvue
Normal file
@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<view>
|
||||
<!-- <view style="height: 500px;"></view> -->
|
||||
<l-camera style="height:750rpx; background-color: aqua;" @error="onError" :focus="true" @click="onFocus" :flash="flash" :device-position="device" ></l-camera>
|
||||
<!-- <l-camera style="height:750rpx; background-color: aqua;" @error="onError" flash="off" device-position="front"></l-camera> -->
|
||||
<image v-if="imagePath" :src="imagePath"></image>
|
||||
<button @click="toggleFlash">切换闪光灯</button>
|
||||
<button @click="toggledevice">切换前后置</button>
|
||||
<button @click="takePhoto">拍照</button>
|
||||
<button @click="setZoom">设置缩放</button>
|
||||
<button @click="startRecord">开始录像</button>
|
||||
<button @click="stopRecord">结束录像</button>
|
||||
<button @click="startFrame">开启监听</button>
|
||||
<button @click="stopFrame">关闭监听</button>
|
||||
</view>
|
||||
</template>
|
||||
<script setup>
|
||||
import {
|
||||
createCameraContext,
|
||||
TakePhotoOption,
|
||||
TakePhotoSuccessCallbackResult,
|
||||
CameraContextSetZoomOption,
|
||||
SetZoomSuccessCallbackResult,
|
||||
CameraContextStartRecordOption,
|
||||
GeneralCallbackResult,
|
||||
CameraContextStopRecordOption,
|
||||
StopRecordSuccessCallbackResult
|
||||
} from '@/uni_modules/lime-camera'
|
||||
// import { fileToDataURL } from '@/uni_modules/lime-file-utils';
|
||||
const context = createCameraContext()
|
||||
|
||||
const flash = ref('off')
|
||||
const device = ref('back')
|
||||
const imagePath = ref('')
|
||||
const onError = (err:any) => {
|
||||
console.log('err::::123', err)
|
||||
}
|
||||
|
||||
const toggleFlash = ()=>{
|
||||
flash.value = flash.value == 'on' ? 'off' : 'on'
|
||||
}
|
||||
const toggledevice = ()=>{
|
||||
device.value = device.value == 'back' ? 'front' : 'back'
|
||||
}
|
||||
const takePhoto = ()=>{
|
||||
let time = Date.now()
|
||||
context.takePhoto({
|
||||
success: (res:TakePhotoSuccessCallbackResult)=> {
|
||||
console.log('takePhoto time', Date.now() - time)
|
||||
imagePath.value = res.tempImagePath
|
||||
console.log('takePhoto', res.tempImagePath)
|
||||
}
|
||||
} as TakePhotoOption)
|
||||
}
|
||||
const setZoom = ()=>{
|
||||
context.setZoom({
|
||||
zoom: Math.random() * 10,
|
||||
success: (res:SetZoomSuccessCallbackResult)=> {
|
||||
console.log('setZoom', res.errMsg, res.zoom)
|
||||
}
|
||||
} as CameraContextSetZoomOption)
|
||||
}
|
||||
|
||||
const startRecord = ()=>{
|
||||
context.startRecord({
|
||||
success(_: GeneralCallbackResult){
|
||||
console.log('startRecord')
|
||||
},
|
||||
fail(err) {
|
||||
console.log('startRecord', err)
|
||||
}
|
||||
} as CameraContextStartRecordOption)
|
||||
}
|
||||
const stopRecord = ()=>{
|
||||
context.stopRecord({
|
||||
success(result: StopRecordSuccessCallbackResult){
|
||||
console.log('stopRecord1', result)
|
||||
console.log('stopRecord2', result.tempThumbPath)
|
||||
},
|
||||
fail(err) {
|
||||
console.log('stopRecord', err)
|
||||
}
|
||||
} as CameraContextStopRecordOption)
|
||||
}
|
||||
let a = context.onCameraFrame((frame)=>{
|
||||
console.log('frame', frame)
|
||||
})
|
||||
const startFrame = ()=>{
|
||||
a.start()
|
||||
}
|
||||
const stopFrame = ()=>{
|
||||
a.stop()
|
||||
}
|
||||
|
||||
const onFocus = (e:UTSJSONObject)=>{
|
||||
console.log('点击了', e)
|
||||
}
|
||||
|
||||
</script>
|
||||
<style>
|
||||
</style>
|
||||
114
uni_modules/lime-camera/components/lime-camera/lime-camera.vue
Normal file
114
uni_modules/lime-camera/components/lime-camera/lime-camera.vue
Normal file
@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<view>
|
||||
|
||||
<l-camera
|
||||
ref="cameraRef"
|
||||
style="height:750rpx; background-color: aqua;"
|
||||
@error="onError"
|
||||
:focus="true"
|
||||
@photoSuccess="onPhotoSuccesss"
|
||||
@recordSuccess="onRecordSuccess"
|
||||
@click="onFocus"
|
||||
:flash="flash"
|
||||
:devicePosition="device"></l-camera>
|
||||
<!-- <l-camera style="height:750rpx; background-color: aqua;" @error="onError" flash="off" device-position="front"></l-camera> -->
|
||||
<image v-if="imagePath" :src="imagePath"></image>
|
||||
<button @click="toggleFlash">切换闪光灯</button>
|
||||
<button @click="toggledevice">切换前后置</button>
|
||||
<button @click="takePhoto">拍照</button>
|
||||
<button @click="setZoom">设置缩放</button>
|
||||
<button @click="startRecord">开始录像</button>
|
||||
<button @click="stopRecord">结束录像</button>
|
||||
<!-- <button @click="startFrame">开启监听</button> -->
|
||||
<!-- <button @click="stopFrame">关闭监听</button> -->
|
||||
</view>
|
||||
</template>
|
||||
<script>
|
||||
import {
|
||||
createCameraContext
|
||||
} from '@/uni_modules/lime-camera'
|
||||
// import { fileToDataURL } from '@/uni_modules/lime-file-utils';
|
||||
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
flash: 'off',
|
||||
device: 'back',
|
||||
imagePath: '',
|
||||
context: null,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.context = createCameraContext()
|
||||
},
|
||||
methods: {
|
||||
onError(err) {
|
||||
console.log('err::::123', err)
|
||||
},
|
||||
toggleFlash() {
|
||||
this.flash = this.flash == 'on' ? 'off' : 'on'
|
||||
console.log('this.flash', this.flash)
|
||||
},
|
||||
toggledevice() {
|
||||
this.device = this.device == 'back' ? 'front' : 'back'
|
||||
},
|
||||
takePhoto() {
|
||||
let time = Date.now()
|
||||
|
||||
// this.$refs.cameraRef?.takePhoto()
|
||||
this.context?.takePhoto({
|
||||
success(res){
|
||||
console.log('res', res)
|
||||
}
|
||||
})
|
||||
},
|
||||
setZoom() {
|
||||
this.context?.setZoom({
|
||||
zoom: Math.random() * 10,
|
||||
success(res){
|
||||
console.log('res', res)
|
||||
}
|
||||
})
|
||||
},
|
||||
startRecord() {
|
||||
// this.$refs.cameraRef?.startRecord()
|
||||
this.context?.startRecord({
|
||||
success(res){
|
||||
console.log('res', res)
|
||||
}
|
||||
})
|
||||
},
|
||||
stopRecord() {
|
||||
// this.$refs.cameraRef?.stopRecord()
|
||||
this.context?.stopRecord({
|
||||
success(res){
|
||||
console.log('res', res)
|
||||
}
|
||||
})
|
||||
},
|
||||
onFocus() {
|
||||
console.log('点击了')
|
||||
},
|
||||
onPhotoSuccesss(res) {
|
||||
console.log('onPhotoSuccesss res', res)
|
||||
},
|
||||
onRecordSuccess(res) {
|
||||
console.log('onRecordSuccess res', res)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// let a = context.onCameraFrame((frame)=>{
|
||||
// console.log('frame', frame)
|
||||
// })
|
||||
// const startFrame = ()=>{
|
||||
// a.start()
|
||||
// }
|
||||
// const stopFrame = ()=>{
|
||||
// a.stop()
|
||||
// }
|
||||
</script>
|
||||
<style>
|
||||
</style>
|
||||
2
uni_modules/lime-camera/encrypt
Normal file
2
uni_modules/lime-camera/encrypt
Normal file
@ -0,0 +1,2 @@
|
||||
úÔçáSrF™èàü”N„>ŸŠÉS(Z`§Û``Šr¥cï.ŠyÁïWæ÷r)S”½©S‚¥?¿ÓŽE «êz›öÆv/–s¯|zÜÛª´
|
||||
P.™,L¥9™=r’ùFÔ:•#h»t‘‘~¿~ܤ;?mL<6D>ˆ©N+þééüä0€a·„<C2B7>vÿÑ™O
|
||||
104
uni_modules/lime-camera/package.json
Normal file
104
uni_modules/lime-camera/package.json
Normal file
@ -0,0 +1,104 @@
|
||||
{
|
||||
"id": "lime-camera",
|
||||
"displayName": "lime-camera 相机",
|
||||
"version": "0.1.0",
|
||||
"description": "lime-camera是参照小程序的camera组件和createCameraContext API实现的uts相机插件,目前只支持安卓",
|
||||
"keywords": [
|
||||
"lime-camera",
|
||||
"createCameraContext",
|
||||
"camera",
|
||||
"相机",
|
||||
"uts"
|
||||
],
|
||||
"repository": "",
|
||||
"engines": {
|
||||
"HBuilderX": "^4.04",
|
||||
"uni-app": "^4.73",
|
||||
"uni-app-x": "^4.74"
|
||||
},
|
||||
"dcloudext": {
|
||||
"type": "component-uts",
|
||||
"sale": {
|
||||
"regular": {
|
||||
"price": "68.00"
|
||||
},
|
||||
"sourcecode": {
|
||||
"price": "168.00"
|
||||
}
|
||||
},
|
||||
"contact": {
|
||||
"qq": ""
|
||||
},
|
||||
"declaration": {
|
||||
"ads": "无",
|
||||
"data": "无",
|
||||
"permissions": "<uses-permission android:name=\"android.permission.CAMERA\" />\n\t<uses-permission android:name=\"android.permission.RECORD_AUDIO\" />\n\t<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\" />\n\t<uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\" />"
|
||||
},
|
||||
"npmurl": "",
|
||||
"darkmode": "x",
|
||||
"i18n": "x",
|
||||
"widescreen": "x"
|
||||
},
|
||||
"uni_modules": {
|
||||
"dependencies": [],
|
||||
"encrypt": [],
|
||||
"platforms": {
|
||||
"cloud": {
|
||||
"tcb": "√",
|
||||
"aliyun": "√",
|
||||
"alipay": "√"
|
||||
},
|
||||
"client": {
|
||||
"uni-app": {
|
||||
"vue": {
|
||||
"vue2": "-",
|
||||
"vue3": "-"
|
||||
},
|
||||
"web": {
|
||||
"safari": "-",
|
||||
"chrome": "-"
|
||||
},
|
||||
"app": {
|
||||
"vue": "-",
|
||||
"nvue": "√",
|
||||
"android": "-",
|
||||
"ios": "-",
|
||||
"harmony": "-"
|
||||
},
|
||||
"mp": {
|
||||
"weixin": "-",
|
||||
"alipay": "-",
|
||||
"toutiao": "-",
|
||||
"baidu": "-",
|
||||
"kuaishou": "-",
|
||||
"jd": "-",
|
||||
"harmony": "-",
|
||||
"qq": "-",
|
||||
"lark": "-"
|
||||
},
|
||||
"quickapp": {
|
||||
"huawei": "-",
|
||||
"union": "-"
|
||||
}
|
||||
},
|
||||
"uni-app-x": {
|
||||
"web": {
|
||||
"safari": "-",
|
||||
"chrome": "-"
|
||||
},
|
||||
"app": {
|
||||
"android": {
|
||||
"extVersion": "",
|
||||
"minVersion": "21"
|
||||
},
|
||||
"ios": "-",
|
||||
"harmony": "-"
|
||||
},
|
||||
"mp": {
|
||||
"weixin": "-"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
114
uni_modules/lime-camera/readme.md
Normal file
114
uni_modules/lime-camera/readme.md
Normal file
@ -0,0 +1,114 @@
|
||||
# lime-camera 相机
|
||||
- 参照小程序的`camera`组件和`createCameraContext`API实现。
|
||||
|
||||
## 安装
|
||||
导入插件后,自定义基座再使用,请先试用后谨慎购买,一但购买没有退货。
|
||||
|
||||
### 基础使用
|
||||
|
||||
```html
|
||||
<l-camera style="height:750rpx; background-color: aqua;" @error="onError" :flash="flash" :device-position="device"></l-camera>
|
||||
<image v-if="imagePath" :src="imagePath"></image>
|
||||
<button @click="toggleFlash">切换闪光灯</button>
|
||||
<button @click="toggledevice">切换前后置</button>
|
||||
<button @click="takePhoto">拍照</button>
|
||||
<button @click="setZoom">设置缩放</button>
|
||||
<button @click="startRecord">开始录像</button>
|
||||
<button @click="stopRecord">结束录像</button>
|
||||
<button @click="startFrame">开启监听</button>
|
||||
<button @click="stopFrame">关闭监听</button>
|
||||
```
|
||||
|
||||
```js
|
||||
import {
|
||||
createCameraContext,
|
||||
TakePhotoOption,
|
||||
TakePhotoSuccessCallbackResult,
|
||||
CameraContextSetZoomOption,
|
||||
SetZoomSuccessCallbackResult,
|
||||
CameraContextStartRecordOption,
|
||||
GeneralCallbackResult,
|
||||
CameraContextStopRecordOption,
|
||||
StopRecordSuccessCallbackResult
|
||||
} from '@/uni_modules/lime-camera'
|
||||
const context = createCameraContext()
|
||||
|
||||
const flash = ref('off')
|
||||
const device = ref('back')
|
||||
const imagePath = ref('')
|
||||
const onError = (err:any) => {
|
||||
console.log('err', err)
|
||||
}
|
||||
|
||||
const toggleFlash = ()=>{
|
||||
flash.value = flash.value == 'on' ? 'off' : 'on'
|
||||
}
|
||||
const toggledevice = ()=>{
|
||||
device.value = device.value == 'back' ? 'front' : 'back'
|
||||
}
|
||||
const takePhoto = ()=>{
|
||||
let time = Date.now()
|
||||
context.takePhoto({
|
||||
success: (res:TakePhotoSuccessCallbackResult)=> {
|
||||
console.log('takePhoto time', Date.now() - time)
|
||||
imagePath.value = res.tempImagePath
|
||||
console.log('takePhoto', res.tempImagePath)
|
||||
}
|
||||
} as TakePhotoOption)
|
||||
}
|
||||
const setZoom = ()=>{
|
||||
context.setZoom({
|
||||
zoom: Math.random() * 10,
|
||||
success: (res:SetZoomSuccessCallbackResult)=> {
|
||||
console.log('setZoom', res.errMsg, res.zoom)
|
||||
}
|
||||
} as CameraContextSetZoomOption)
|
||||
}
|
||||
|
||||
const startRecord = ()=>{
|
||||
context.startRecord({
|
||||
success(res: GeneralCallbackResult){
|
||||
console.log('startRecord')
|
||||
}
|
||||
} as CameraContextStartRecordOption)
|
||||
}
|
||||
const stopRecord = ()=>{
|
||||
context.stopRecord({
|
||||
success(result: StopRecordSuccessCallbackResult){
|
||||
console.log('stopRecord', result.tempThumbPath)
|
||||
}
|
||||
} as CameraContextStopRecordOption)
|
||||
}
|
||||
let listener = context.onCameraFrame()
|
||||
const startFrame = ()=>{
|
||||
listener.start()
|
||||
}
|
||||
const stopFrame = ()=>{
|
||||
listener.stop()
|
||||
}
|
||||
```
|
||||
|
||||
## Props
|
||||
因为直接参照小程序`camera`组件,所以可以直接按[camera](https://uniapp.dcloud.net.cn/component/camera.html)文档来。但不支持扫码。扫码可以使用[lime-scan](https://ext.dcloud.net.cn/plugin?id=16452)
|
||||
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
| --------------------------| ------------------------------------------------------------ | ---------------- | ------------ |
|
||||
| focus | 是否开启点击对焦 | <em>boolean</em> | `false` |
|
||||
|
||||
## 事件
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
| --------------------------| ------------------------------------------------------------ | ---------------- | ------------ |
|
||||
| @click | (event:UniEvent) => {} | <em>UniEvent</em> | |
|
||||
|
||||
|
||||
## API
|
||||
因为直接参照小程序`createcameracontext`API,所以可以直接按[createcameracontext](https://uniapp.dcloud.net.cn/api/media/camera-context.html#createcameracontext)文档来。<br>
|
||||
但`onCameraFrame`里的回调中的data为`ImageProxy`
|
||||
```ts
|
||||
context.onCameraFrame((frame)=>{
|
||||
// 这里是 ImageProxy
|
||||
frame.data
|
||||
})
|
||||
```
|
||||
@ -0,0 +1,14 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="cn.limeui.camera">
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-feature android:name="android.hardware.camera" android:required="true" />
|
||||
<uses-feature android:name="android.hardware.camera.any" />
|
||||
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.camera.flash" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.microphone" android:required="true" />
|
||||
<application android:requestLegacyExternalStorage="true">
|
||||
<meta-data android:name="ScopedStorage" android:value="true" />
|
||||
</application>
|
||||
</manifest>
|
||||
BIN
uni_modules/lime-camera/utssdk/app-android/camera.uts
Normal file
BIN
uni_modules/lime-camera/utssdk/app-android/camera.uts
Normal file
Binary file not shown.
20
uni_modules/lime-camera/utssdk/app-android/config.json
Normal file
20
uni_modules/lime-camera/utssdk/app-android/config.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"minSdkVersion": "21",
|
||||
"dependencies": [
|
||||
// 使用camera2实现的CameraX核心库
|
||||
// 定义camerax_version为"1.4.0-alpha04"
|
||||
// 下面的行是可选的,因为core库通过camera-camera2间接包含
|
||||
"androidx.camera:camera-core:1.4.0-alpha04"
|
||||
"androidx.camera:camera-camera2:1.4.0-alpha04"
|
||||
// 如果你想要额外使用CameraX生命周期库
|
||||
"androidx.camera:camera-lifecycle:1.4.0-alpha04"
|
||||
// 如果你想要额外使用CameraX视频捕获库
|
||||
"androidx.camera:camera-video:1.4.0-alpha04"
|
||||
// 如果你想要额外使用CameraX视图类
|
||||
"androidx.camera:camera-view:1.4.0-alpha04"
|
||||
// 如果你想要额外添加CameraX ML Kit Vision集成
|
||||
// "androidx.camera:camera-mlkit-vision:1.4.0-alpha04"
|
||||
// 如果你想要额外使用CameraX扩展库
|
||||
// "androidx.camera:camera-extensions:1.4.0-alpha04"
|
||||
]
|
||||
}
|
||||
BIN
uni_modules/lime-camera/utssdk/app-android/index.uts
Normal file
BIN
uni_modules/lime-camera/utssdk/app-android/index.uts
Normal file
Binary file not shown.
274
uni_modules/lime-camera/utssdk/app-android/index.vue
Normal file
274
uni_modules/lime-camera/utssdk/app-android/index.vue
Normal file
@ -0,0 +1,274 @@
|
||||
<template>
|
||||
<view></view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
/**
|
||||
* 引用 Android 系统库
|
||||
* [可选实现,按需引入]
|
||||
*/
|
||||
import PreviewView from 'androidx.camera.view.PreviewView'
|
||||
import View from 'android.view.View';
|
||||
import { LimeCamera } from './camera';
|
||||
import { CameraConfig, TakePhotoOption,CameraContextStopRecordOption, TakePhotoCallback, CameraContextSetZoomOption, CameraContextStartRecordOption } from '../interface'
|
||||
/**
|
||||
* 引入三方库
|
||||
* [可选实现,按需引入]
|
||||
*
|
||||
* 在 Android 平台引入三方库有以下两种方式:
|
||||
* 1、[推荐] 通过 仓储 方式引入,将 三方库的依赖信息 配置到 config.json 文件下的 dependencies 字段下。详细配置方式[详见](https://uniapp.dcloud.net.cn/plugin/uts-plugin.html#dependencies)
|
||||
* 2、直接引入,将 三方库的aar或jar文件 放到libs目录下。更多信息[详见](https://uniapp.dcloud.net.cn/plugin/uts-plugin.html#android%E5%B9%B3%E5%8F%B0%E5%8E%9F%E7%94%9F%E9%85%8D%E7%BD%AE)
|
||||
*
|
||||
* 在通过上述任意方式依赖三方库后,使用时需要在文件中 import
|
||||
* import { LottieAnimationView } from 'com.airbnb.lottie.LottieAnimationView'
|
||||
*/
|
||||
|
||||
/**
|
||||
* UTSAndroid 为平台内置对象,不需要 import 可直接调用其API,[详见](https://uniapp.dcloud.net.cn/uts/utsandroid.html#utsandroid)
|
||||
*/
|
||||
|
||||
//原生提供以下属性或方法的实现
|
||||
export default {
|
||||
/**
|
||||
* 组件名称,也就是开发者使用的标签
|
||||
*/
|
||||
name: "l-camera",
|
||||
/**
|
||||
* 组件涉及的事件声明,只有声明过的事件,才能被正常发送
|
||||
*/
|
||||
emits: ['stop', 'error', 'initdone', 'ready', 'scancode', 'click', 'photoSuccess', 'recordSuccess'],
|
||||
/**
|
||||
* 属性声明,组件的使用者会传递这些属性值到组件
|
||||
*/
|
||||
props: {
|
||||
"mode": {
|
||||
type: String,
|
||||
default: "normal"
|
||||
},
|
||||
"resolution": {
|
||||
type: String,
|
||||
default: "medium" // low | high
|
||||
},
|
||||
"devicePosition": {
|
||||
type: String,
|
||||
default: "back" // front前置 |back 后置
|
||||
},
|
||||
"flash": {
|
||||
type: String,
|
||||
default: "auto" // auto, on, off, torch
|
||||
},
|
||||
"frameSize": {
|
||||
type: String,
|
||||
default: "medium" // small|large
|
||||
},
|
||||
"focus": {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 组件内部变量声明
|
||||
*/
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
/**
|
||||
* 属性变化监听器实现
|
||||
*/
|
||||
watch: {
|
||||
"devicePosition": {
|
||||
/**
|
||||
* 这里监听属性变化,并进行组件内部更新
|
||||
*/
|
||||
handler(newValue : string, oldValue : string) {
|
||||
if (oldValue == newValue) return
|
||||
this.$el?.switchCamera(newValue)
|
||||
},
|
||||
immediate: false // 创建时是否通过此方法更新属性,默认值为false
|
||||
},
|
||||
"flash": {
|
||||
/**
|
||||
* 这里监听属性变化,并进行组件内部更新
|
||||
*/
|
||||
handler(newValue : string, oldValue : string) {
|
||||
if (oldValue == newValue) return
|
||||
this.$el?.setFlash(newValue)
|
||||
},
|
||||
immediate: false // 创建时是否通过此方法更新属性,默认值为false
|
||||
|
||||
},
|
||||
"focus": {
|
||||
/**
|
||||
* 这里监听属性变化,并进行组件内部更新
|
||||
*/
|
||||
handler(newValue : boolean, oldValue : boolean) {
|
||||
if (oldValue == newValue) return
|
||||
this.$el?.setFocus(newValue)
|
||||
},
|
||||
immediate: false // 创建时是否通过此方法更新属性,默认值为false
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 规则:如果没有配置expose,则methods中的方法均对外暴露,如果配置了expose,则以expose的配置为准向外暴露
|
||||
* ['publicMethod'] 含义为:只有 `publicMethod` 在实例上可用
|
||||
*/
|
||||
expose: ['takePhoto', 'setZoom', 'startRecord', 'stopRecord'],
|
||||
methods: {
|
||||
/**
|
||||
* 对外公开的组件方法
|
||||
*
|
||||
* uni-app中调用示例:
|
||||
* this.$refs["组件ref"].doSomething("uts-button");
|
||||
*
|
||||
* uni-app x中调用示例:
|
||||
* 1、引入对应Element
|
||||
* import { UtsButtonElement(组件名称以upper camel case方式命名 + Element) } from 'uts.sdk.modules.utsComponent(组件目录名称以lower camel case方式命名)';
|
||||
* 2、(this.$refs["组件ref"] as UtsButtonElement).doSomething("uts-button");
|
||||
* 或 (uni.getElementById("组件id") as UtsButtonElement).doSomething("uts-button");
|
||||
*/
|
||||
takePhoto() {
|
||||
this.$el?.takePhoto({
|
||||
success(res) {
|
||||
this.$emit('photoSuccess', res)
|
||||
},
|
||||
fail(err) {
|
||||
this.$emit('error', err)
|
||||
console.log('takePhoto err', err)
|
||||
}
|
||||
} as TakePhotoOption)
|
||||
},
|
||||
setZoom(zoom: number) {
|
||||
this.$el?.setZoom({
|
||||
zoom: zoom,
|
||||
} as CameraContextSetZoomOption)
|
||||
},
|
||||
startRecord() {
|
||||
this.$el?.startRecord({
|
||||
// selfieMirror: options.getBoolean('selfieMirror'),
|
||||
// timeout: options.getNumber('timeout'),
|
||||
success(res) {
|
||||
console.log('startRecord success', res)
|
||||
},
|
||||
fail(err) {
|
||||
console.log('startRecord err', err)
|
||||
}
|
||||
} as CameraContextStartRecordOption)
|
||||
},
|
||||
stopRecord() {
|
||||
this.$el?.stopRecord({
|
||||
success(res) {
|
||||
console.log('stopRecord success', res)
|
||||
this.$emit('recordSuccess', res)
|
||||
},
|
||||
fail(err) {
|
||||
console.log('stopRecord fail', err)
|
||||
this.$emit('error', err)
|
||||
},
|
||||
} as CameraContextStopRecordOption)
|
||||
},
|
||||
},
|
||||
/**
|
||||
* [可选实现] 组件被创建,组件第一个生命周期,
|
||||
* 在内存中被占用的时候被调用,开发者可以在这里执行一些需要提前执行的初始化逻辑
|
||||
*/
|
||||
created() {
|
||||
|
||||
},
|
||||
/**
|
||||
* [可选实现] 对应平台的view载体即将被创建,对应前端beforeMount
|
||||
*/
|
||||
NVBeforeLoad() {
|
||||
|
||||
},
|
||||
/**
|
||||
* [必须实现] 创建原生View,必须定义返回值类型
|
||||
* 开发者需要重点实现这个函数,声明原生组件被创建出来的过程,以及最终生成的原生组件类型
|
||||
* (Android需要明确知道View类型,需特殊校验)
|
||||
*/
|
||||
NVLoad() : LimeCamera {
|
||||
let previewView = new LimeCamera(
|
||||
this.$androidContext!,
|
||||
this as UTSComponent<FrameLayout>)
|
||||
previewView.setTag("limeCamera");
|
||||
return previewView
|
||||
|
||||
},
|
||||
/**
|
||||
* [可选实现] 原生View已创建
|
||||
*/
|
||||
NVLoaded() {
|
||||
|
||||
},
|
||||
/**
|
||||
* [可选实现] 原生View布局完成
|
||||
*/
|
||||
NVLayouted() {
|
||||
this.$el?.setConfig({
|
||||
resolution: this.resolution,
|
||||
frameSize: this.frameSize,
|
||||
flash: this.flash,
|
||||
focus: this.focus,
|
||||
} as CameraConfig)
|
||||
this.$el?.switchCamera(this.devicePosition)
|
||||
},
|
||||
/**
|
||||
* [可选实现] 原生View将释放
|
||||
*/
|
||||
NVBeforeUnload() {
|
||||
this.$emit('stop')
|
||||
this.$el?.unbindCamera()
|
||||
},
|
||||
/**
|
||||
* [可选实现] 原生View已释放,这里可以做释放View之后的操作
|
||||
*/
|
||||
NVUnloaded() {
|
||||
|
||||
},
|
||||
/**
|
||||
* [可选实现] 组件销毁
|
||||
*/
|
||||
unmounted() {
|
||||
|
||||
},
|
||||
/**
|
||||
* [可选实现] 自定组件布局尺寸,用于告诉排版系统,组件自身需要的宽高
|
||||
* 一般情况下,组件的宽高应该是由终端系统的排版引擎决定,组件开发者不需要实现此函数
|
||||
* 但是部分场景下,组件开发者需要自己维护宽高,则需要开发者重写此函数
|
||||
*/
|
||||
NVMeasure(size : UTSSize) : UTSSize {
|
||||
// size.width = 300.0.toFloat();
|
||||
// size.height = 200.0.toFloat();
|
||||
return size;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 定义按钮点击后触发回调的类
|
||||
* [可选实现]
|
||||
*/
|
||||
class ButtonClickListener extends View.OnClickListener {
|
||||
/**
|
||||
* 如果需要在回调类或者代理类中对组件进行操作,比如调用组件方法,发送事件等,需要在该类中持有组件对应的原生类的对象
|
||||
* 组件原生类的基类为 UTSComponent,该类是一个泛型类,需要接收一个类型变量,该类型变量就是原生组件的类型
|
||||
*/
|
||||
private comp : UTSComponent<Button>;
|
||||
|
||||
constructor(comp : UTSComponent<Button>) {
|
||||
super();
|
||||
this.comp = comp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按钮点击回调方法
|
||||
*/
|
||||
override onClick(v ?: View) {
|
||||
console.log("按钮被点击");
|
||||
// 发送事件
|
||||
this.comp.$emit("buttonclick");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
372
uni_modules/lime-camera/utssdk/app-harmony/builder.ets
Normal file
372
uni_modules/lime-camera/utssdk/app-harmony/builder.ets
Normal file
@ -0,0 +1,372 @@
|
||||
import { abilityAccessCtrl, Permissions } from '@kit.AbilityKit';
|
||||
import { camera } from '@kit.CameraKit';
|
||||
import { BusinessError } from '@kit.BasicServicesKit';
|
||||
import { image } from '@kit.ImageKit';
|
||||
import fs from '@ohos.file.fs';
|
||||
|
||||
@Component
|
||||
struct CameraViewComponent {
|
||||
@Prop mode: string = 'photo'
|
||||
@Prop isFront: boolean = false
|
||||
@Prop isFlash: boolean = false
|
||||
@Prop isTorch: boolean = false
|
||||
@Prop correctOrientation: boolean = false
|
||||
@Prop @Watch('onCommandChange') command: string = '' // 指令
|
||||
|
||||
onTake?: (path: string) => void
|
||||
|
||||
private xComponentCtl: XComponentController = new XComponentController();
|
||||
private xComponentSurfaceId: string = '';
|
||||
@State imageWidth: number = 1920;
|
||||
@State imageHeight: number = 1080;
|
||||
private cameraManager: camera.CameraManager | undefined = undefined;
|
||||
private cameras: Array<camera.CameraDevice> | Array<camera.CameraDevice> = [];
|
||||
private cameraPosition: number = 0
|
||||
private isFrontCamera = false
|
||||
private cameraInput: camera.CameraInput | undefined = undefined;
|
||||
private previewOutput: camera.PreviewOutput | undefined = undefined;
|
||||
private photoOutput: camera.PhotoOutput | undefined = undefined;
|
||||
private videoSession: camera.VideoSession | undefined = undefined;
|
||||
private photoSession: camera.PhotoSession | undefined = undefined;
|
||||
private uiContext: UIContext = this.getUIContext();
|
||||
private context: Context | undefined = this.uiContext.getHostContext();
|
||||
private cameraPermission: Permissions = 'ohos.permission.CAMERA';
|
||||
private isTorchOn: boolean = false
|
||||
private isFlashOn: boolean = false
|
||||
@State isShow: boolean = false;
|
||||
|
||||
onCommandChange() {
|
||||
let command = this.command.split('-')[0]
|
||||
if (command == 'open') {
|
||||
this.initCamera()
|
||||
} else if (command == 'close') {
|
||||
this.releaseCamera();
|
||||
} else if (command == 'take') {
|
||||
this.takePhoto()
|
||||
} else if (command == 'switch') {
|
||||
this.isFrontCamera = this.isFront
|
||||
this.switchCamera()
|
||||
} else if (command == 'flash') {
|
||||
this.isFlashOn = this.isFlash
|
||||
this.onFlash()
|
||||
} else if (command == 'torch') {
|
||||
this.isTorchOn = this.isTorch
|
||||
this.onTorch()
|
||||
}
|
||||
}
|
||||
|
||||
async requestPermissionsFn(): Promise<void> {
|
||||
let atManager = abilityAccessCtrl.createAtManager();
|
||||
if (this.context) {
|
||||
let res = await atManager.requestPermissionsFromUser(this.context, [this.cameraPermission]);
|
||||
for (let i = 0; i < res.permissions.length; i++) {
|
||||
if (this.cameraPermission.toString() === res.permissions[i] && res.authResults[i] === 0) {
|
||||
this.isShow = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async aboutToAppear() {
|
||||
await this.requestPermissionsFn();
|
||||
}
|
||||
|
||||
aboutToDisappear(): void {
|
||||
this.releaseCamera();
|
||||
}
|
||||
|
||||
// 初始化相机。
|
||||
async initCamera(): Promise<void> {
|
||||
// console.info(`initCamera previewOutput xComponentSurfaceId:${this.xComponentSurfaceId}`);
|
||||
try {
|
||||
// 获取相机管理器实例。
|
||||
this.cameraManager = camera.getCameraManager(this.context);
|
||||
if (!this.cameraManager) {
|
||||
console.error('initCamera getCameraManager');
|
||||
}
|
||||
// 获取当前设备支持的相机device列表。
|
||||
this.cameras = this.cameraManager.getSupportedCameras();
|
||||
if (!this.cameras) {
|
||||
console.error('initCamera getSupportedCameras');
|
||||
}
|
||||
// 选择一个相机device,创建cameraInput输出对象。
|
||||
this.cameraInput = this.cameraManager.createCameraInput(this.cameras[this.cameraPosition]);
|
||||
if (!this.cameraInput) {
|
||||
console.error('initCamera createCameraInput');
|
||||
}
|
||||
// 打开相机。
|
||||
await this.cameraInput.open().catch((err: BusinessError) => {
|
||||
console.error(`initCamera open fail: ${err}`);
|
||||
})
|
||||
// 获取相机device支持的profile。
|
||||
let capability: camera.CameraOutputCapability = this.cameraManager.getSupportedOutputCapability(this.cameras[this.cameraPosition], camera.SceneMode.NORMAL_PHOTO);
|
||||
if (!capability) {
|
||||
console.error('initCamera getSupportedOutputCapability');
|
||||
}
|
||||
|
||||
let previewProfilesArray: Array<camera.Profile> = capability.previewProfiles;
|
||||
if (!previewProfilesArray) {
|
||||
console.error("createOutput previewProfilesArray == null || undefined");
|
||||
}
|
||||
|
||||
let photoProfilesArray: Array<camera.Profile> = capability.photoProfiles;
|
||||
if (!photoProfilesArray) {
|
||||
console.error("createOutput photoProfilesArray == null || undefined");
|
||||
}
|
||||
|
||||
// 预览
|
||||
let minRatioDiff: number = 0.1;
|
||||
let surfaceRatio: number = this.imageWidth / this.imageHeight; // 最接近16:9宽高比。
|
||||
let previewProfile: camera.Profile = previewProfilesArray[0];
|
||||
|
||||
// // 应用开发者根据实际业务需求选择一个支持的预览流previewProfile。
|
||||
// for (let index = 0; index < previewProfilesArray.length; index++) {
|
||||
// const tempProfile = previewProfilesArray[index];
|
||||
// let tempRatio = tempProfile.size.width >= tempProfile.size.height ?
|
||||
// tempProfile.size.width / tempProfile.size.height : tempProfile.size.height / tempProfile.size.width;
|
||||
// let currentRatio = Math.abs(tempRatio - surfaceRatio);
|
||||
// if (currentRatio <= minRatioDiff && tempProfile.format == camera.CameraFormat.CAMERA_FORMAT_JPEG) {
|
||||
// previewProfile = tempProfile;
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// this.imageWidth = previewProfile.size.width; // 更新xComponent组件的宽。
|
||||
// this.imageHeight = previewProfile.size.height; // 更新xComponent组件的高。
|
||||
// console.info(`initCamera imageWidth:${this.imageWidth} imageHeight:${this.imageHeight}`);
|
||||
|
||||
// 使用xComponentSurfaceId创建预览。
|
||||
this.previewOutput = this.cameraManager.createPreviewOutput(previewProfile, this.xComponentSurfaceId);
|
||||
if (!this.previewOutput) {
|
||||
console.error('initCamera createPreviewOutput');
|
||||
}
|
||||
|
||||
// 拍照
|
||||
this.photoOutput = this.cameraManager.createPhotoOutput(photoProfilesArray[0]);
|
||||
this.setPhotoOutputCb(this.photoOutput);
|
||||
|
||||
// 创建录像模式相机会话。
|
||||
if(this.mode == 'video') {
|
||||
this.videoSession = this.cameraManager.createSession(camera.SceneMode.NORMAL_VIDEO) as camera.VideoSession;
|
||||
if (!this.videoSession) {
|
||||
console.error('initCamera createSession');
|
||||
}
|
||||
// 开始配置会话。
|
||||
this.videoSession.beginConfig();
|
||||
// 添加相机设备输入。
|
||||
this.videoSession.addInput(this.cameraInput);
|
||||
// 添加预览流输出。
|
||||
this.videoSession.addOutput(this.previewOutput);
|
||||
// 提交会话配置。
|
||||
await this.videoSession.commitConfig();
|
||||
// 开始启动已配置的输入输出流。
|
||||
await this.videoSession.start();
|
||||
} else if(this.mode == 'photo') {
|
||||
this.photoSession = this.cameraManager.createSession(camera.SceneMode.NORMAL_PHOTO) as camera.PhotoSession;
|
||||
if (!this.photoSession) {
|
||||
console.error('initCamera createSession');
|
||||
}
|
||||
// 开始配置会话。
|
||||
this.photoSession.beginConfig();
|
||||
// 添加相机设备输入。
|
||||
this.photoSession.addInput(this.cameraInput);
|
||||
// 添加预览流输出。
|
||||
this.photoSession.addOutput(this.previewOutput);
|
||||
// 添加拍照输出流
|
||||
this.photoSession.addOutput(this.photoOutput);
|
||||
// 提交会话配置。
|
||||
await this.photoSession.commitConfig();
|
||||
// 开始启动已配置的输入输出流。
|
||||
await this.photoSession.start();
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`initCamera fail: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 释放相机。
|
||||
async releaseCamera(): Promise<void> {
|
||||
try {
|
||||
if(this.mode == 'video') {
|
||||
// 停止当前会话。
|
||||
await this.videoSession?.stop();
|
||||
// 释放相机输入流。
|
||||
await this.cameraInput?.close();
|
||||
// 释放预览输出流。
|
||||
await this.previewOutput?.release();
|
||||
// 释放会话。
|
||||
await this.videoSession?.release();
|
||||
} else if(this.mode == 'photo') {
|
||||
// 停止当前会话。
|
||||
await this.photoSession?.stop();
|
||||
// 释放相机输入流。
|
||||
await this.cameraInput?.close();
|
||||
// 释放预览输出流。
|
||||
await this.previewOutput?.release();
|
||||
// 释放拍照输出流。
|
||||
await this.photoOutput?.release();
|
||||
// 释放会话。
|
||||
await this.photoSession?.release();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`initCamera fail: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
setPhotoOutputCb(photoOutput: camera.PhotoOutput): void {
|
||||
//设置回调之后,调用photoOutput的capture方法,就会将拍照的buffer回传到回调中。
|
||||
photoOutput.on('photoAvailable', (errCode: BusinessError, photo: camera.Photo): void => {
|
||||
if (errCode || photo === undefined) {
|
||||
console.error('getPhoto failed');
|
||||
return;
|
||||
}
|
||||
let imageObj = photo.main;
|
||||
imageObj.getComponent(image.ComponentType.JPEG, async (errCode: BusinessError, component: image.Component) => {
|
||||
if (errCode || component === undefined) {
|
||||
console.error('getComponent failed');
|
||||
return;
|
||||
}
|
||||
let buffer: ArrayBuffer;
|
||||
if (component.byteBuffer) {
|
||||
buffer = component.byteBuffer;
|
||||
} else {
|
||||
console.error('byteBuffer is null');
|
||||
return;
|
||||
}
|
||||
|
||||
// 生成照片
|
||||
try {
|
||||
const tempDir = this.context?.cacheDir;
|
||||
|
||||
const timestamp = new Date().getTime();
|
||||
const fileName = `photo_${timestamp}.jpg`;
|
||||
const filePath = `${tempDir}/${fileName}`;
|
||||
|
||||
const file = await fs.open(filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
|
||||
await fs.write(file.fd, buffer);
|
||||
await fs.close(file.fd);
|
||||
|
||||
this.onTake?.(filePath);
|
||||
|
||||
} catch (error) {
|
||||
console.error('保存临时照片失败:', error);
|
||||
} finally {
|
||||
imageObj.release();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
takePhoto() {
|
||||
let photoCaptureSetting: camera.PhotoCaptureSetting = {
|
||||
quality: camera.QualityLevel.QUALITY_LEVEL_HIGH, // 设置图片质量高。
|
||||
rotation: camera.ImageRotation.ROTATION_0 // 设置图片旋转角度0。
|
||||
}
|
||||
|
||||
// 使用当前拍照设置进行拍照。
|
||||
this.photoOutput?.capture(photoCaptureSetting, (err: BusinessError) => {
|
||||
if (err) {
|
||||
console.error(`Failed to capture the photo ${err.message}`);
|
||||
return;
|
||||
}
|
||||
// console.info('Callback invoked to indicate the photo capture request success.');
|
||||
});
|
||||
}
|
||||
|
||||
// 切换摄像头
|
||||
async switchCamera() {
|
||||
if (this.cameras.length < 2) {
|
||||
console.error('Only one camera available');
|
||||
return;
|
||||
}
|
||||
|
||||
// 切换摄像头位置
|
||||
this.cameraPosition = (this.isFrontCamera
|
||||
? camera.CameraPosition.CAMERA_POSITION_FRONT
|
||||
: camera.CameraPosition.CAMERA_POSITION_BACK) - 1;
|
||||
|
||||
await this.releaseCamera();
|
||||
await this.initCamera();
|
||||
}
|
||||
|
||||
// 手电筒
|
||||
onTorch() {
|
||||
try {
|
||||
let torchSupport = this.cameraManager?.isTorchSupported() ?? false
|
||||
if(torchSupport) {
|
||||
if(this.cameraPosition == camera.CameraPosition.CAMERA_POSITION_BACK - 1) {
|
||||
console.error('back camera not support');
|
||||
return
|
||||
}
|
||||
|
||||
this.cameraManager?.setTorchMode(this.isTorchOn ? camera.TorchMode.ON : camera.TorchMode.OFF);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
// 闪光灯
|
||||
onFlash() {
|
||||
try {
|
||||
let torchSupport = this.cameraManager?.isTorchSupported() ?? false
|
||||
if(torchSupport) {
|
||||
if(this.cameraPosition == camera.CameraPosition.CAMERA_POSITION_FRONT - 1) {
|
||||
console.error('front camera not support');
|
||||
return
|
||||
}
|
||||
|
||||
this.cameraManager?.setTorchMode(this.isFlashOn ? camera.TorchMode.AUTO : camera.TorchMode.OFF);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
build() {
|
||||
Column() {
|
||||
if (this.isShow) {
|
||||
XComponent({
|
||||
id: 'componentId',
|
||||
type: XComponentType.SURFACE,
|
||||
controller: this.xComponentCtl
|
||||
})
|
||||
.onLoad(async () => {
|
||||
// 获取组件surfaceId。
|
||||
this.xComponentSurfaceId = this.xComponentCtl.getXComponentSurfaceId();
|
||||
// 初始化相机,组件实时渲染每帧预览流数据。
|
||||
this.initCamera()
|
||||
})
|
||||
.width(this.uiContext.px2vp(this.imageHeight))
|
||||
.height(this.uiContext.px2vp(this.imageWidth))
|
||||
}
|
||||
}
|
||||
.justifyContent(FlexAlign.Center)
|
||||
.backgroundColor(Color.Black)
|
||||
.height('100%')
|
||||
.width('100%')
|
||||
}
|
||||
}
|
||||
|
||||
@Builder
|
||||
export function CameraView(params: ESObject) {
|
||||
Row() {
|
||||
CameraViewComponent({
|
||||
mode: 'photo',
|
||||
isFront: params.isFront,
|
||||
isFlash: params.isFlash,
|
||||
isTorch: params.isTorch,
|
||||
command: params.command,
|
||||
correctOrientation: params.correctOrientation,
|
||||
onTake: (path: string) => {
|
||||
let fun = params.onTake as (path: string) => void
|
||||
fun(path)
|
||||
}
|
||||
})
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.attributeModifier(params.attributeUpdater)
|
||||
}
|
||||
BIN
uni_modules/lime-camera/utssdk/app-harmony/index.uts
Normal file
BIN
uni_modules/lime-camera/utssdk/app-harmony/index.uts
Normal file
Binary file not shown.
27
uni_modules/lime-camera/utssdk/app-harmony/module.json5
Normal file
27
uni_modules/lime-camera/utssdk/app-harmony/module.json5
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"module": {
|
||||
"name": "uni_modules__ux_camera_view_har",
|
||||
"type": "har",
|
||||
"deviceTypes": [
|
||||
"default",
|
||||
"tablet",
|
||||
"2in1"
|
||||
],
|
||||
"requestPermissions": [
|
||||
{
|
||||
"name": "ohos.permission.CAMERA",
|
||||
"reason": "$string:permission_CAMERA_reason",
|
||||
"usedScene": {
|
||||
"when": "inuse"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ohos.permission.MICROPHONE",
|
||||
"reason": "$string:permission_MICROPHONE_reason",
|
||||
"usedScene": {
|
||||
"when": "inuse"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
{
|
||||
"string": [
|
||||
{
|
||||
"name": "permission_CAMERA_reason",
|
||||
"value": "需要相机权限来实现拍摄功能"
|
||||
},
|
||||
{
|
||||
"name": "permission_MICROPHONE_reason",
|
||||
"value": "需要录音权限来实现拍摄录音功能"
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
uni_modules/lime-camera/utssdk/app-ios/camera.uts
Normal file
BIN
uni_modules/lime-camera/utssdk/app-ios/camera.uts
Normal file
Binary file not shown.
BIN
uni_modules/lime-camera/utssdk/app-ios/index.uts
Normal file
BIN
uni_modules/lime-camera/utssdk/app-ios/index.uts
Normal file
Binary file not shown.
266
uni_modules/lime-camera/utssdk/app-ios/index.vue
Normal file
266
uni_modules/lime-camera/utssdk/app-ios/index.vue
Normal file
@ -0,0 +1,266 @@
|
||||
<template>
|
||||
<view class="defaultStyles">
|
||||
</view>
|
||||
</template>
|
||||
<script lang="uts">
|
||||
/**
|
||||
* 引用 iOS 系统库
|
||||
* [可选实现,按需引入]
|
||||
*/
|
||||
import {
|
||||
UIButton,
|
||||
UIControl
|
||||
} from "UIKit"
|
||||
|
||||
/**
|
||||
* 引入三方库
|
||||
* [可选实现,按需引入]
|
||||
*
|
||||
* 在 iOS 平台引入三方库有以下两种方式:
|
||||
* 1、通过引入三方库framework 或者.a 等方式,需要将 .framework 放到 ./Frameworks 目录下,将.a 放到 ./Libs 目录下。更多信息[详见](https://uniapp.dcloud.net.cn/plugin/uts-plugin.html#ios-平台原生配置)
|
||||
* 2、通过 cocoaPods 方式引入,将要引入的 pod 信息配置到 config.json 文件下的 dependencies-pods 字段下。详细配置方式[详见](https://uniapp.dcloud.net.cn/plugin/uts-ios-cocoapods.html)
|
||||
*
|
||||
* 在通过上述任意方式依赖三方库后,使用时需要在文件中 import:
|
||||
* 示例:import { LottieAnimationView, LottieAnimation, LottieLoopMode } from 'Lottie'
|
||||
*/
|
||||
|
||||
/**
|
||||
* UTSiOS、UTSComponent 为平台内置对象,不需要 import 可直接调用其API,[详见](https://uniapp.dcloud.net.cn/uts/utsios.html)
|
||||
*/
|
||||
import { UTSComponent } from "DCloudUTSFoundation"
|
||||
import { LimeCamera } from './camera'
|
||||
//原生提供以下属性或方法的实现
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
};
|
||||
},
|
||||
/**
|
||||
* 组件名称,也就是开发者使用的标签
|
||||
*/
|
||||
name: "l-camera",
|
||||
/**
|
||||
* 组件涉及的事件声明,只有声明过的事件,才能被正常发送
|
||||
*/
|
||||
emits: ['stop', 'error', 'initdone', 'ready', 'scancode', 'click', 'photoSuccess', 'recordSuccess'],
|
||||
|
||||
/**
|
||||
* 属性声明,组件的使用者会传递这些属性值到组件
|
||||
*/
|
||||
props: {
|
||||
"mode": {
|
||||
type: String,
|
||||
default: "normal"
|
||||
},
|
||||
"resolution": {
|
||||
type: String,
|
||||
default: "medium" // low | high
|
||||
},
|
||||
"devicePosition": {
|
||||
type: String,
|
||||
default: "back" // front前置 |back 后置
|
||||
},
|
||||
"flash": {
|
||||
type: String,
|
||||
default: "auto" // auto, on, off, torch
|
||||
},
|
||||
"frameSize": {
|
||||
type: String,
|
||||
default: "medium" // small|large
|
||||
},
|
||||
"focus": {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 组件内部变量声明
|
||||
*/
|
||||
|
||||
/**
|
||||
* 属性变化监听器实现
|
||||
*/
|
||||
watch: {
|
||||
"devicePosition": {
|
||||
/**
|
||||
* 这里监听属性变化,并进行组件内部更新
|
||||
*/
|
||||
handler(newValue : string, oldValue : string) {
|
||||
if (oldValue == newValue) return
|
||||
this.$el?.switchCamera(newValue)
|
||||
},
|
||||
immediate: false // 创建时是否通过此方法更新属性,默认值为false
|
||||
},
|
||||
"flash": {
|
||||
/**
|
||||
* 这里监听属性变化,并进行组件内部更新
|
||||
*/
|
||||
handler(newValue : string, oldValue : string) {
|
||||
if (oldValue == newValue) return
|
||||
this.$el?.setFlash(newValue)
|
||||
},
|
||||
immediate: false // 创建时是否通过此方法更新属性,默认值为false
|
||||
|
||||
},
|
||||
"focus": {
|
||||
/**
|
||||
* 这里监听属性变化,并进行组件内部更新
|
||||
*/
|
||||
handler(newValue : boolean, oldValue : boolean) {
|
||||
if (oldValue == newValue) return
|
||||
this.$el?.setFocus(newValue)
|
||||
},
|
||||
immediate: false // 创建时是否通过此方法更新属性,默认值为false
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 规则:如果没有配置expose,则methods中的方法均对外暴露,如果配置了expose,则以expose的配置为准向外暴露
|
||||
* ['publicMethod'] 含义为:只有 `publicMethod` 在实例上可用
|
||||
*/
|
||||
expose: ['takePhoto', 'setZoom', 'startRecord', 'stopRecord'],
|
||||
methods: {
|
||||
takePhoto() {
|
||||
this.$el?.takePhoto({
|
||||
success(res) {
|
||||
this.$emit('photoSuccess', res)
|
||||
},
|
||||
fail(err) {
|
||||
this.$emit('error', err)
|
||||
console.log('takePhoto err', err)
|
||||
}
|
||||
} as TakePhotoOption)
|
||||
},
|
||||
setZoom(zoom: number) {
|
||||
this.$el?.setZoom({
|
||||
zoom: zoom,
|
||||
} as CameraContextSetZoomOption)
|
||||
},
|
||||
startRecord() {
|
||||
this.$el?.startRecord({
|
||||
// selfieMirror: options.getBoolean('selfieMirror'),
|
||||
// timeout: options.getNumber('timeout'),
|
||||
success(res) {
|
||||
console.log('startRecord success', res)
|
||||
},
|
||||
fail(err) {
|
||||
console.log('startRecord err', err)
|
||||
}
|
||||
} as CameraContextStartRecordOption)
|
||||
},
|
||||
stopRecord() {
|
||||
this.$el?.stopRecord({
|
||||
success(res) {
|
||||
console.log('stopRecord success', res)
|
||||
this.$emit('recordSuccess', res)
|
||||
},
|
||||
fail(err) {
|
||||
console.log('stopRecord fail', err)
|
||||
this.$emit('error', err)
|
||||
},
|
||||
} as CameraContextStopRecordOption)
|
||||
},
|
||||
},
|
||||
/**
|
||||
* 组件被创建,组件第一个生命周期,
|
||||
* 在内存中被占用的时候被调用,开发者可以在这里执行一些需要提前执行的初始化逻辑
|
||||
* [可选实现]
|
||||
*/
|
||||
created() {
|
||||
|
||||
},
|
||||
/**
|
||||
* 对应平台的view载体即将被创建,对应前端beforeMount
|
||||
* [可选实现]
|
||||
*/
|
||||
NVBeforeLoad() {
|
||||
|
||||
},
|
||||
/**
|
||||
* 创建原生View,必须定义返回值类型
|
||||
* 开发者需要重点实现这个函数,声明原生组件被创建出来的过程,以及最终生成的原生组件类型
|
||||
* [必须实现]
|
||||
*/
|
||||
NVLoad() : LimeCamera {
|
||||
const view = new LimeCamera()
|
||||
view.comp = this
|
||||
return view
|
||||
},
|
||||
|
||||
/**
|
||||
* 原生View已创建
|
||||
* [可选实现]
|
||||
*/
|
||||
NVLoaded() {
|
||||
/**
|
||||
* 通过 this.$el 来获取原生控件。
|
||||
*/
|
||||
// camrea.setView(this.$el!)
|
||||
},
|
||||
/**
|
||||
* 原生View布局完成
|
||||
* [可选实现]
|
||||
*/
|
||||
NVLayouted() {
|
||||
|
||||
},
|
||||
/**
|
||||
* 原生View将释放
|
||||
* [可选实现]
|
||||
*/
|
||||
NVBeforeUnload() { },
|
||||
/**
|
||||
* 原生View已释放,这里可以做释放View之后的操作
|
||||
* [可选实现]
|
||||
*/
|
||||
NVUnloaded() {
|
||||
this.$el?.unbindCamera()
|
||||
},
|
||||
/**
|
||||
* 组件销毁
|
||||
* [可选实现]
|
||||
*/
|
||||
unmounted() { }
|
||||
|
||||
/**
|
||||
* 更多组件开发的信息详见:https://uniapp.dcloud.net.cn/plugin/uts-component.html
|
||||
*/
|
||||
}
|
||||
|
||||
/**
|
||||
* 定义按钮点击后触发回调的类
|
||||
* [可选实现]
|
||||
*/
|
||||
class ButtonClickListsner {
|
||||
/**
|
||||
* 如果需要在回调类或者代理类中对组件进行操作,比如调用组件方法,发送事件等,需要在该类中持有组件对应的原生类的对象。
|
||||
* 组件原生类的基类为 UTSComponent,该类是一个泛型类,需要接收一个类型变量,该类型变量就是原生组件的类型。
|
||||
*/
|
||||
private component : UTSComponent<UIButton>
|
||||
|
||||
constructor(component : UTSComponent<UIButton>) {
|
||||
this.component = component
|
||||
super.init()
|
||||
}
|
||||
|
||||
/**
|
||||
* 按钮点击回调方法
|
||||
* 在 swift 中,所有target-action (例如按钮的点击事件,NotificationCenter 的通知事件等)对应的 action 函数前面都要使用 @objc 进行标记。
|
||||
* [可选实现]
|
||||
*/
|
||||
@objc buttonClickAction() {
|
||||
console.log("按钮被点击")
|
||||
// 发送事件
|
||||
this.component.__$$emit("buttonclick");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 定义回调类或者代理类的实例
|
||||
* [可选实现]
|
||||
*/
|
||||
let buttonClickListsner : ButtonClickListsner | null = null
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
14
uni_modules/lime-camera/utssdk/app-ios/info.plist
Normal file
14
uni_modules/lime-camera/utssdk/app-ios/info.plist
Normal file
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>我们需要您的相机权限来拍照或录像。</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>需要使用你的相册进行选择视频及图片</string>
|
||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||
<string>需要保存图片和视频至你的相册中</string>
|
||||
<key>PHPhotoLibraryPreventAutomaticLimitedAccessAlert</key>
|
||||
<string>需要保存图片和视频至你的相册中</string>
|
||||
</dict>
|
||||
</plist>
|
||||
227
uni_modules/lime-camera/utssdk/interface.uts
Normal file
227
uni_modules/lime-camera/utssdk/interface.uts
Normal file
@ -0,0 +1,227 @@
|
||||
// #ifdef APP-ANDROID
|
||||
import ImageProxy from 'androidx.camera.core.ImageProxy';
|
||||
// #endif
|
||||
|
||||
export type GeneralCallbackResult = {
|
||||
/** 错误信息 */
|
||||
errMsg : string
|
||||
}
|
||||
export type TakePhotoSuccessCallbackResult = {
|
||||
/** 照片文件的临时路径 (本地路径),安卓是jpg图片格式,ios是png */
|
||||
tempImagePath : string
|
||||
errMsg : string
|
||||
}
|
||||
|
||||
export type TakePhotoCallback = (res: string) => void
|
||||
|
||||
/** 接口调用失败的回调函数 */
|
||||
export type TakePhotoFailCallback = (res : GeneralCallbackResult) => void
|
||||
/** 接口调用成功的回调函数 */
|
||||
export type TakePhotoSuccessCallback = (
|
||||
result : TakePhotoSuccessCallbackResult
|
||||
) => void
|
||||
|
||||
/** 接口调用结束的回调函数(调用成功、失败都会执行) */
|
||||
export type TakePhotoCompleteCallback = (res : GeneralCallbackResult) => void
|
||||
|
||||
export type TakePhotoOption = {
|
||||
/** 接口调用结束的回调函数(调用成功、失败都会执行) */
|
||||
complete ?: TakePhotoCompleteCallback
|
||||
/** 接口调用失败的回调函数 */
|
||||
fail ?: TakePhotoFailCallback
|
||||
/** 成像质量
|
||||
*
|
||||
* 可选值:
|
||||
* - 'high': 高质量;
|
||||
* - 'normal': 普通质量;
|
||||
* - 'low': 低质量;
|
||||
* - 'original': 原图; */
|
||||
quality ?: 'high' | 'normal' | 'low' | 'original'
|
||||
/**
|
||||
* 是否开启镜像 */
|
||||
selfieMirror ?: boolean
|
||||
/** 接口调用成功的回调函数 */
|
||||
success ?: TakePhotoSuccessCallback
|
||||
}
|
||||
|
||||
|
||||
export type SetZoomSuccessCallbackResult = {
|
||||
/** 实际设置的缩放级别。由于系统限制,某些机型可能无法设置成指定值,会改用最接近的可设值。 */
|
||||
zoom : number
|
||||
errMsg : string
|
||||
}
|
||||
/** 接口调用结束的回调函数(调用成功、失败都会执行) */
|
||||
export type SetZoomCompleteCallback = (res : GeneralCallbackResult) => void
|
||||
/** 接口调用失败的回调函数 */
|
||||
export type SetZoomFailCallback = (res : GeneralCallbackResult) => void
|
||||
/** 接口调用成功的回调函数 */
|
||||
export type CameraContextSetZoomSuccessCallback = (
|
||||
result : SetZoomSuccessCallbackResult
|
||||
) => void
|
||||
|
||||
export type CameraContextSetZoomOption = {
|
||||
/** 缩放级别,范围[1, maxZoom]。zoom 可取小数,精确到小数后一位。maxZoom 可在 bindinitdone 返回值中获取。 */
|
||||
zoom : number
|
||||
/** 接口调用结束的回调函数(调用成功、失败都会执行) */
|
||||
complete ?: SetZoomCompleteCallback
|
||||
/** 接口调用失败的回调函数 */
|
||||
fail ?: SetZoomFailCallback
|
||||
/** 接口调用成功的回调函数 */
|
||||
success ?: CameraContextSetZoomSuccessCallback
|
||||
}
|
||||
|
||||
export type StartRecordTimeoutCallbackResult = {
|
||||
/** 封面图片文件的临时路径 (本地路径) */
|
||||
tempThumbPath : string
|
||||
/** 视频的文件的临时路径 (本地路径) */
|
||||
tempVideoPath : string
|
||||
}
|
||||
|
||||
/** 接口调用结束的回调函数(调用成功、失败都会执行) */
|
||||
export type StartRecordCompleteCallback = (res : GeneralCallbackResult) => void
|
||||
/** 接口调用失败的回调函数 */
|
||||
export type StartRecordFailCallback = (res : GeneralCallbackResult) => void
|
||||
/** 超过录制时长上限时会结束录像并触发此回调,录像异常退出时也会触发此回调 */
|
||||
export type StartRecordTimeoutCallback = (
|
||||
result : StartRecordTimeoutCallbackResult
|
||||
) => void
|
||||
/** 接口调用成功的回调函数 */
|
||||
export type CameraContextStartRecordSuccessCallback = (
|
||||
res : GeneralCallbackResult
|
||||
) => void
|
||||
export type CameraContextStartRecordOption = {
|
||||
/** 接口调用结束的回调函数(调用成功、失败都会执行) */
|
||||
complete ?: StartRecordCompleteCallback
|
||||
/** 接口调用失败的回调函数 */
|
||||
fail ?: StartRecordFailCallback
|
||||
/**
|
||||
* 是否开启镜像 */
|
||||
selfieMirror ?: boolean
|
||||
/** 接口调用成功的回调函数 */
|
||||
success ?: CameraContextStartRecordSuccessCallback
|
||||
/**
|
||||
* 录制时长上限,单位为秒,最长不能超过 5 分钟 */
|
||||
timeout ?: number
|
||||
/** 超过录制时长上限时会结束录像并触发此回调,录像异常退出时也会触发此回调 */
|
||||
timeoutCallback ?: StartRecordTimeoutCallback
|
||||
}
|
||||
|
||||
export type StopRecordSuccessCallbackResult = {
|
||||
/** 封面图片文件的临时路径 (本地路径) */
|
||||
tempThumbPath : string
|
||||
/** 视频的文件的临时路径 (本地路径) */
|
||||
tempVideoPath : string
|
||||
errMsg : string
|
||||
}
|
||||
/** 接口调用结束的回调函数(调用成功、失败都会执行) */
|
||||
export type StopRecordCompleteCallback = (res : GeneralCallbackResult) => void
|
||||
/** 接口调用失败的回调函数 */
|
||||
export type StopRecordFailCallback = (res : GeneralCallbackResult) => void
|
||||
/** 接口调用成功的回调函数 */
|
||||
export type CameraContextStopRecordSuccessCallback = (
|
||||
result : StopRecordSuccessCallbackResult
|
||||
) => void
|
||||
export type CameraContextStopRecordOption = {
|
||||
/** 接口调用结束的回调函数(调用成功、失败都会执行) */
|
||||
complete ?: StopRecordCompleteCallback
|
||||
/** 启动视频压缩,压缩效果同`chooseVideo` */
|
||||
compressed ?: boolean
|
||||
/** 接口调用失败的回调函数 */
|
||||
fail ?: StopRecordFailCallback
|
||||
/** 接口调用成功的回调函数 */
|
||||
success ?: CameraContextStopRecordSuccessCallback
|
||||
}
|
||||
|
||||
/** 回调函数 */
|
||||
export type OnCameraFrameCallback = (result : OnCameraFrameCallbackResult) => void
|
||||
export type OnCameraFrameCallbackResult = {
|
||||
/** 图像像素点数据,一维数组,每四项表示一个像素点的 rgba */
|
||||
// #ifdef APP-ANDROID
|
||||
data : ImageProxy
|
||||
// #endif
|
||||
// #ifndef APP-ANDROID
|
||||
data : ArrayBuffer
|
||||
// #endif
|
||||
/** 图像数据矩形的高度 */
|
||||
height : number
|
||||
/** 图像数据矩形的宽度 */
|
||||
width : number
|
||||
}
|
||||
/** 接口调用成功的回调函数 */
|
||||
export type StartSuccessCallback = (res : GeneralCallbackResult) => void
|
||||
/** 接口调用失败的回调函数 */
|
||||
type StartFailCallback = (res : GeneralCallbackResult) => void
|
||||
/** 接口调用结束的回调函数(调用成功、失败都会执行) */
|
||||
type StartCompleteCallback = (res : GeneralCallbackResult) => void
|
||||
export type CameraFrameListenerStartOption = {
|
||||
/** 接口调用结束的回调函数(调用成功、失败都会执行) */
|
||||
complete ?: StartCompleteCallback
|
||||
/** 接口调用失败的回调函数 */
|
||||
fail ?: StartFailCallback
|
||||
/** 接口调用成功的回调函数 */
|
||||
success ?: StartSuccessCallback
|
||||
/** [Worker](https://developers.weixin.qq.com/miniprogram/dev/api/worker/Worker.html)
|
||||
*
|
||||
* 可选参数。如果需要在 iOS ExperimentalWorker 内监听摄像头帧数据,则需要传入对应 Worker 对象。详情 [Worker.getCameraFrameData](https://developers.weixin.qq.com/miniprogram/dev/api/worker/Worker.getCameraFrameData.html) */
|
||||
// worker ?: Worker
|
||||
}
|
||||
|
||||
|
||||
/** 接口调用结束的回调函数(调用成功、失败都会执行) */
|
||||
export type StopCompleteCallback = (res : GeneralCallbackResult) => void
|
||||
/** 接口调用失败的回调函数 */
|
||||
export type StopFailCallback = (res : GeneralCallbackResult) => void
|
||||
/** 接口调用成功的回调函数 */
|
||||
export type StopSuccessCallback = (res : GeneralCallbackResult) => void
|
||||
export type StopOption = {
|
||||
/** 接口调用结束的回调函数(调用成功、失败都会执行) */
|
||||
complete ?: StopCompleteCallback
|
||||
/** 接口调用失败的回调函数 */
|
||||
fail ?: StopFailCallback
|
||||
/** 接口调用成功的回调函数 */
|
||||
success ?: StopSuccessCallback
|
||||
}
|
||||
|
||||
// export type CameraFrameListener = {
|
||||
// /**
|
||||
// * 开始监听帧数据 */
|
||||
// start(option ?: CameraFrameListenerStartOption) : void
|
||||
// /**
|
||||
// * 停止监听帧数据 */
|
||||
// stop(option ?: StopOption) : void
|
||||
// }
|
||||
|
||||
export type FlashMode = 'auto' | 'on' | 'off' | 'torch'
|
||||
export type DevicePosition = 'back' | 'front'
|
||||
export type Resolution = 'low' | 'medium' | 'high'
|
||||
export type FrameSize = 'medium' | 'small' | 'large'
|
||||
// type OutputDimension = '360P' | '540P' | '720P' | '1080P' | 'max'
|
||||
export type QualityType = 'high' | 'normal' | 'low' | 'original'
|
||||
|
||||
|
||||
export type CameraConfig = {
|
||||
flash ?: FlashMode
|
||||
devicePosition ?: DevicePosition
|
||||
resolution ?: Resolution
|
||||
frameSize ?: FrameSize
|
||||
focus ?: boolean
|
||||
// outputDimension ?: OutputDimension
|
||||
}
|
||||
|
||||
|
||||
export interface CameraFrameListener {
|
||||
start():void
|
||||
start(option ?: CameraFrameListenerStartOption):void
|
||||
stop():void
|
||||
stop(option ?: StopOption):void
|
||||
}
|
||||
|
||||
|
||||
export interface CameraContext {
|
||||
takePhoto(option : TakePhotoOption):void
|
||||
setZoom(option : CameraContextSetZoomOption):void
|
||||
startRecord(option : CameraContextStartRecordOption):void
|
||||
stopRecord(option : CameraContextStopRecordOption) :void
|
||||
onCameraFrame():CameraFrameListener
|
||||
onCameraFrame(callback : OnCameraFrameCallback | null) : CameraFrameListener
|
||||
}
|
||||
BIN
uni_modules/lime-camera/utssdk/web/index.uts
Normal file
BIN
uni_modules/lime-camera/utssdk/web/index.uts
Normal file
Binary file not shown.
71
uni_modules/lime-camera/utssdk/web/index.vue
Normal file
71
uni_modules/lime-camera/utssdk/web/index.vue
Normal file
@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<view id="l-camera">
|
||||
<text>web 不支持</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "l-camera",
|
||||
emits: ['stop', 'error', 'initdone','ready','scancode'],
|
||||
props: {
|
||||
"mode": {
|
||||
type: String,
|
||||
default: "normal"
|
||||
},
|
||||
"resolution": {
|
||||
type: String,
|
||||
default: "medium" // low | high
|
||||
},
|
||||
"devicePosition": {
|
||||
type: String,
|
||||
default: "back" // front前置 |back 后置
|
||||
},
|
||||
"flash": {
|
||||
type: String,
|
||||
default: "auto" // auto, on, off, torch
|
||||
},
|
||||
"frameSize": {
|
||||
type: String,
|
||||
default: "medium" // small|large
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
"devicePosition": {
|
||||
/**
|
||||
* 这里监听属性变化,并进行组件内部更新
|
||||
*/
|
||||
handler(newValue : string, oldValue : string) {
|
||||
if(oldValue == newValue) return
|
||||
// this.$el?.switchCamera(newValue)
|
||||
},
|
||||
immediate: false // 创建时是否通过此方法更新属性,默认值为false
|
||||
},
|
||||
"flash": {
|
||||
/**
|
||||
* 这里监听属性变化,并进行组件内部更新
|
||||
*/
|
||||
handler(newValue : string, oldValue : string) {
|
||||
if(oldValue == newValue) return
|
||||
// this.$el?.setFlash(newValue)
|
||||
},
|
||||
immediate: false // 创建时是否通过此方法更新属性,默认值为false
|
||||
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
init(){
|
||||
return '11'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
Loading…
Reference in New Issue
Block a user