在微信小程序开发中,相机功能已成为许多应用的核心组成部分。本文将介绍如何使用UniApp框架实现一个功能丰富的相机组件,支持拍照、录像、前后摄像头切换以及双指缩放等功能。
功能概述
这个相机组件具备以下核心功能:
拍照功能:支持高质量图片拍摄
录像功能:支持最长60秒的视频录制
前后摄像头切换:轻松切换前置和后置摄像头
双指缩放:通过手势控制相机变焦
操作模式切换:在拍照和录像模式间流畅切换
实现细节
相机基础设置
首先,我们使用UniApp的
camera
组件作为基础双指缩放实现
双指缩放是通过监听触摸事件并计算两指之间的距离变化来实现的
模式切换与媒体捕获
通过滑动切换拍照和录像模式,并分别实现拍照和录像功能
用户界面设计
组件界面采用黑色主题,符合相机应用的常见设计风格
样式设计
使用SCSS编写样式,确保界面美观且响应式
完整代码如下
<template>
<view class="container">
<camera
:device-position="cameraType"
flash="off"
@error="handleCameraError"
@touchstart="handleZoomStart"
@touchmove="handleZoomMove"
style="width: 100%; height: 75vh"
ref="cameraRef"
:resolution="'high'"
></camera>
<!-- 缩放级别显示(可选) -->
<view class="zoom-indicator">缩放: {{ currentZoom.toFixed(1) }}x</view>
<view class="btn_group">
<view
class="top"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
style="height: 100rpx"
>
<view
:style="{
color: currentIndex === index ? 'yellow' : '#FFFFFF',
transform: `translateX(${currentIndex === index ? '20rpx' : '0'})`,
}"
class="top_item"
v-for="(item, index) in typeList"
:key="index"
>
{{ item?.name }}
</view>
</view>
<view class="bottom">
<image
@tap="handlePreviewImage(photoPath, [photoPath])"
v-if="photoPath"
class="pic"
:src="photoPath"
/>
<view v-else class="pic">暂无</view>
<view
><uni-icons
@click="handleClick"
:color="isRecord ? '#ff0000' : `#ffffff`"
type="circle-filled"
size="56"
></uni-icons
></view>
<view>
<uni-icons
@click="handleLoop"
type="loop"
color="#ffffff"
size="40"
></uni-icons
></view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { reactive, ref, onMounted, watch } from "vue";
import { handlePreviewImage } from "@/utils/common";
// 相机实例
const cameraRef = ref(null);
// 照片路径
const photoPath = ref("");
//摄像头类型
const cameraType = ref<"back" | "front">("back");
//记录loop切换的类型
const loopFlag = ref(false);
//记录是否开始录制
const isRecord = ref(false);
// 缩放相关
const initialDistance = ref(0); // 初始双指距离
const currentZoom = ref(1); // 当前缩放级别(初始1)
const maxZoom = 2.5; // 最大缩放级别
const minZoom = 1; // 最小缩放级别
//操作类型
const typeList = reactive([
{
name: "拍照",
type: 1,
},
{
name: "录像",
type: 2,
},
]);
const currentIndex = ref(0);
const startX = ref(1);
const endX = ref(1);
// 双指缩放逻辑
const handleZoomStart = (e: TouchEvent) => {
if (e.touches.length >= 2) {
initialDistance.value = Math.hypot(
e.touches[0].clientX - e.touches[1].clientX,
e.touches[0].clientY - e.touches[1].clientY
);
}
};
const handleZoomMove = (e: TouchEvent) => {
if (e.touches.length >= 2 && initialDistance.value > 0) {
const currentDistance = Math.hypot(
e.touches[0].clientX - e.touches[1].clientX,
e.touches[0].clientY - e.touches[1].clientY
);
// 计算缩放变化(更平滑的算法)
const zoomDelta = (currentDistance - initialDistance.value) / 200;
let newZoom = currentZoom.value + zoomDelta;
newZoom = Math.max(minZoom, Math.min(maxZoom, newZoom));
if (newZoom !== currentZoom.value) {
currentZoom.value = newZoom;
setCameraZoom(newZoom);
}
initialDistance.value = currentDistance;
}
};
// 设置相机缩放
const setCameraZoom = (zoom: number) => {
const cameraContext = uni.createCameraContext();
cameraContext.setZoom({
zoom: zoom,
success: () => console.log("缩放设置成功:", zoom),
fail: (err) => console.error("缩放失败:", err),
});
};
// 初始化时设置默认缩放
onMounted(() => {
setCameraZoom(1); // 初始化为1x
});
// 其他原有方法保持不变(handleTouchStart、handleClick等...)
function handleTouchStart(e: any) {
console.log(e, "start");
startX.value = e.touches[0].clientX;
}
function handleTouchMove(e: any) {
endX.value = e.touches[0].clientX;
}
function handleTouchEnd() {
const diffX = startX.value - endX.value;
if (diffX > 50 && currentIndex.value < typeList.length - 1) {
currentIndex.value++;
} else if (diffX < -50 && currentIndex.value > 0) {
currentIndex.value--;
}
startX.value = endX.value = 0;
}
const handleLoop = () => {
loopFlag.value = !loopFlag.value;
cameraType.value = loopFlag.value ? "front" : "back";
};
const handleClick = () => {
if (currentIndex.value === 0) {
takePhoto();
} else {
isRecord.value ? stopRecord() : startRecord();
}
};
const takePhoto = async () => {
try {
const cameraContext = uni.createCameraContext();
cameraContext.takePhoto({
quality: "high",
success: (res) => {
photoPath.value = res.tempImagePath;
},
fail: console.error,
});
} catch (e) {
console.error("相机异常", e);
}
};
const startRecord = () => {
uni.showToast({ title: "开始录像", duration: 500 });
const cameraContext = uni.createCameraContext();
cameraContext.startRecord({
timeout: 60000,
success: () => (isRecord.value = true),
fail: (err) => {
isRecord.value = false;
console.error("开始录像失败", err);
},
});
};
const stopRecord = () => {
isRecord.value = false;
const cameraContext = uni.createCameraContext();
cameraContext.stopRecord({
success: (res) => {
uni.showToast({ title: "录像结束", duration: 500 });
photoPath.value = res?.tempThumbPath;
},
fail: console.error,
});
};
const handleCameraError = (e: any) => {
console.error("相机错误", e);
};
//如果切换拍照、录像,处于录像状态,则停止录像
watch(currentIndex, (val) => {
if (isRecord.value) {
stopRecord();
}
});
</script>
<style scoped lang="scss">
.container {
display: flex;
flex-direction: column;
align-items: center;
background: black;
color: white;
width: 100vw;
height: 100vh;
position: relative;
}
.zoom-indicator {
position: absolute;
top: 20rpx;
left: 20rpx;
background: rgba(0, 0, 0, 0.5);
color: white;
padding: 10rpx 20rpx;
border-radius: 20rpx;
z-index: 10;
}
.btn_group {
flex: 1;
width: 100vw;
display: flex;
flex-direction: column;
box-sizing: border-box;
.top {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
.top_item {
width: auto;
color: white;
margin-right: 32rpx;
transition: all 0.3s ease;
}
}
.bottom {
margin-top: 20rpx;
flex: 1;
box-sizing: border-box;
padding: 0 60rpx;
display: flex;
align-items: center;
justify-content: space-between;
.pic {
width: 70rpx;
height: 70rpx;
border-radius: 12rpx;
background: white;
color: black;
font-size: 20rpx;
line-height: 70rpx;
text-align: center;
}
}
}
</style>
希望本文对您实现相机功能有所帮助。注意:没有加录制超时逻辑,需要的话自行在startRecord回调中添加!