实现微信小程序的UniApp相机组件:拍照、录像与双指缩放

发布于:2025-08-30 ⋅ 阅读:(18) ⋅ 点赞:(0)

在微信小程序开发中,相机功能已成为许多应用的核心组成部分。本文将介绍如何使用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回调中添加!


网站公告

今日签到

点亮在社区的每一天
去签到