更新记录

1.0.1(2026-09-18) 下载此版本

添加预览图

1.0.0(2026-09-18) 下载此版本

遮罩覆盖层随便自定义样式


平台兼容性

uni-app(3.8.4)

Vue2 Vue3 Chrome Safari app-vue app-nvue Android iOS 鸿蒙
× × - -
微信小程序 支付宝小程序 抖音小程序 百度小程序 快手小程序 京东小程序 鸿蒙元服务 QQ小程序 飞书小程序 小红书小程序 快应用-华为 快应用-联盟
- - - - - - - - - - - -

uni-app x(3.8.11)

Chrome Safari Android iOS 鸿蒙 微信小程序
- - - - - -

lj-costomer-camera 自定义相机组件

基于 Camera2 / AVFoundation 实现的 App 端原生相机预览与拍照组件Android 端使用标准基座即可直接运行调试。

适用于证件拍摄(身份证、银行卡、驾照等)、自定义取景框、需要完全接管相机 UI 的场景。


为什么需要它

  • 原生预览画面(无 webview 性能损耗)
  • 完全自定义的 UI 覆盖层:取景框、遮罩、引导图、提示文案
  • 前后摄切换、闪光灯常亮、预览尺寸指定、画面旋转微调
  • 拍照结果以文件路径回传,不占内存、不卡帧

平台支持

平台 状态 说明
App-Android ✅ 已验证 Camera2 + TextureView,零依赖,标准基座可运行
App-iOS ⚠️ 代码已实现
H5 / 小程序 ❌ 不支持 原生扩展组件仅在 App 平台可用

iOS 端协议方法签名(AVCapturePhotoCaptureDelegate)需按 HBuilderX 实际编译提示微调,欢迎提交反馈。

环境要求

  • HBuilderX 3.6.18+(uni-app 兼容模式 UTS 组件支持)
  • 组件只能用在 nvue 页面(uni-app 原生扩展组件的硬性限制,普通 vue 页面无法承载原生 View)
  • App 端支持 Vue2 / Vue3 工程

安装

插件市场导入后,项目根目录会出现:

uni_modules/lj-costomer-camera/
├─ package.json
└─ utssdk/
   ├─ app-android/
   │  ├─ index.vue 
   │  └─ config.json
   └─ app-ios/
      ├─ index.vue
      ├─ config.json
      └─ info.plist

快速开始

1. 新建 nvue 页面

pages/camera/camera.nvue

<template>
    <view class="page">
        <!-- 组件必须显式给尺寸,flex 对它不生效 -->
        <lj-costomer-camera
            class="cam"
            :style="camStyle"
            :facing="facing"
            :flash="flash"
            :shoot="shootToken"
            :active="pageActive"
            @ready="onCamReady"
            @error="onCamError"
            @photo="onCamPhoto"></lj-costomer-camera>

        <!-- 自定义 UI 覆盖层 -->
        <view class="overlay">
            <view class="band" style="flex: 1;"></view>
            <view class="mid-row">
                <view class="band" style="flex: 1;"></view>
                <view class="frame"></view>
                <view class="band" style="flex: 1;"></view>
            </view>
            <view class="band" style="flex: 1;"></view>
        </view>

        <view class="shutter" @click="takePhoto"></view>
    </view>
</template>

<script>
export default {
    data() {
        return {
            facing: 'back',
            flash: 'off',
            shootToken: 0,
            pageActive: true,
            camReady: false,
            taking: false,
            winW: 750,
            winH: 1400
        };
    },
    computed: {
        camStyle() {
            return { width: this.winW + 'px', height: this.winH + 'px' };
        }
    },
    onLoad() {
        const sys = uni.getSystemInfoSync();
        this.winW = 750;
        this.winH = Math.round(750 * sys.windowHeight / sys.windowWidth);
    },
    onShow() {
        this.pageActive = true;
    },
    onHide() {
        this.pageActive = false;
    },
    methods: {
        // 事件参数被 weex 包在 event.detail 里
        detail(e) {
            return (e && e.detail) ? e.detail : (e || {});
        },
        onCamReady() {
            this.camReady = true;
        },
        onCamError(e) {
            uni.showToast({ title: this.detail(e).msg || '相机异常', icon: 'none' });
        },
        onCamPhoto(e) {
            if (!this.taking) return;
            this.taking = false;
            uni.hideLoading();
            const res = this.detail(e);
            if (res.path) {
                console.log('照片路径:', res.path);
                // 这里是沙盒绝对路径,用 image 展示时需加 file:// 前缀
            }
        },
        takePhoto() {
            if (!this.camReady || this.taking) return;
            this.taking = true;
            uni.showLoading({ title: '处理中' });
            // 令牌递增触发拍照,结果走 @photo 事件
            this.shootToken = this.shootToken + 1;
        }
    }
};
</script>

<style>
.page { flex: 1; background-color: #000000; }
.cam { position: absolute; left: 0px; top: 0px; }
.overlay { position: absolute; left: 0px; top: 0px; right: 0px; bottom: 0px; }
.band { background-color: rgba(0, 0, 0, 0.62); }
.mid-row { flex-direction: row; }
.frame {
    width: 660px;
    height: 416px;
    border-width: 2px;
    border-style: dashed;
    border-color: rgba(255, 255, 255, 0.9);
}
.shutter {
    position: absolute;
    left: 315px;
    bottom: 60px;
    width: 120px;
    height: 120px;
    border-radius: 60px;
    background-color: #ffffff;
}
</style>

2. 注册页面

pages.json

{
    "path": "pages/camera/camera",
    "style": {
        "navigationBarTitleText": "拍照",
        "app-plus": { "titleNView": false }
    }
}

3. 权限配置

Androidmanifest.json 的 App 权限里勾选「相机(CAMERA)」。 组件内部会自动发起运行时权限申请,无需手动调用。

iOS:插件已内置 NSCameraUsageDescription,无需额外配置。


API

Props

名称 类型 默认值 说明
facing String back 摄像头方向:back / front
flash String off 闪光灯:on 为常亮(仅后置有效)
shoot Number 0 拍照触发令牌。每次递增触发一次拍照,结果经 photo 事件回传
active Boolean true 页面是否在前台。false 时释放相机,true 时恢复
rotate Number 0 预览画面额外旋转角度:0 / 90 / 180 / 270。默认 0,由系统处理方向
size Number -1 预览尺寸索引。-1 自动挑选;0~5 依次为 4096x30723280x24642880x21601920x14401440x10801280x960

⚠️ prop 名称必须使用单单词的全小写形式。nvue 对 UTS 组件不做 kebab-case → camelCase 转换,size-index 这类名字传不进组件(这也是为什么属性叫 size 而不是 sizeIndex)。

Events

名称 触发时机 参数(在 event.detail 中)
ready 预览会话配置完成 { msg: 'ready' }
error 相机异常 / 权限被拒 / 拍照失败 { msg: '错误描述' }
photo 拍照完成 { code: 0, path: '文件绝对路径', msg: 'ok' }
debug 内部流程日志(排查用) { msg: '日志内容' }

注意:nvue 的自定义事件参数统一包在 event.detail 里,取值方式:

onCamPhoto(e) {
    const res = (e && e.detail) ? e.detail : e;
    console.log(res.path);
}

组件方法(可选)

也支持通过 ref 调用,但不建议:nvue 环境下 this.$refs 有时取不到原生组件,推荐一律使用「props 下发 + 事件上行」。

方法 说明
takePhoto(options, callback) 拍照,options: { quality: 1-100 }
startPreview() / stopPreview() 手动启停预览
setFlashMode(mode, callback) 设置闪光灯
switchCamera(pos, callback) 切换摄像头
setPreviewRotate(deg, callback) 设置预览旋转角度
setSizeIndex(index) 设置预览尺寸索引
getStatus(callback) 读取内部状态(排查用)

拍照结果说明

  • 返回的是沙盒内绝对路径,例如 …/Android/data/<包名>/files/idcard_1730000000000.jpg
  • <image> 展示时建议补 file:// 前缀:'file://' + path
  • 图片方向会自动旋转为竖屏方向;前置摄像头会做镜像翻转

实现原理

┌─ nvue 页面(UI 层,双端通用)──────────────┐
│  取景框 / 遮罩 / 按钮,全部用前端绘制        │
├─ UTS 组件(原生层,只负责画面与硬件)──────┤
│  Android:Camera2 + TextureView + getBitmap 抓帧 │
│  iOS:    AVCaptureSession + AVCapturePhotoOutput │
└────────────────────────────────────────┘

关键设计:原生层只负责「出画面」和「按快门」,所有 UI 用 nvue 写一份,双端复用。不要把取景框画在原生里,否则要写两遍。

  • Android 拍照:用 TextureView.getBitmap() 抓取预览帧,避免 ImageReader + 会话重建的复杂度。分辨率等于预览尺寸,证件拍摄足够。
  • Android 预览尺寸:优先选 4:3 比例(多数传感器的原生比例,视野更宽),并按设备实际支持列表自动挑选。
  • 零第三方依赖android.hardware.camera2.*AVFoundation 都是系统框架,因此 Android 端无需自定义基座,标准基座即可热刷新调试。

常见问题(重要,均为实战踩坑)

1. 画面全黑、ready 事件不触发

原因:组件没有拿到尺寸。nvue 中 flex: 1 对 UTS 原生组件无效,尺寸为 0 时无法创建 Surface,相机永远不会启动。

解决:必须显式给尺寸。

<lj-costomer-camera :style="{ width: '750px', height: winH + 'px' }"></lj-costomer-camera>

2. 运行崩溃:WX_RENDER_ERR_TEXTURE_SETBACKGROUND

原因:组件根节点直接返回了 TextureView。weex 会对组件根节点调用 setBackground(),而 TextureView 不允许设置背景 drawable。

解决:插件内部已用 FrameLayout 包裹 TextureView,根节点交给 weex 管理。自定义原生组件时同理,根节点不要用 SurfaceView / TextureView

3. 崩溃:fireEvent must be called by main thread

原因:相机回调运行在子线程,而 weex 的 $emit 强制要求主线程调用。

解决:插件内部统一通过 Handler(Looper.getMainLooper()) 切回主线程再发事件。自己写 UTS 组件时,任何回调里发事件都要切主线程

4. this.$refs.xxx 取不到组件,调用方法无反应

原因:nvue 下 $refs 对 UTS 组件不可靠,且调用抛异常时会被静默吞掉(表现为"点了没反应"、loading 卡死)。

解决:改用 props 传参 + 事件回传。需要"触发动作"时用递增令牌模式:

// 页面
this.shootToken = this.shootToken + 1;
// 组件
"shoot": {
    handler(newValue: number, oldValue: number) {
        if (newValue <= this.lastShootToken) return;  // 必须去重
        this.lastShootToken = newValue;
        capture();
    }
}

5. prop 传不进组件

两种原因

  • 名字含大写(如 sizeIndex)—— nvue 不做 kebab 转换,请改用单单词全小写
  • 名字与 CSS 属性冲突(如 position)—— 会被样式系统截走,请改名(本插件用 facing 而非 position

6. 动作被重复执行(拍两张、重启两次)

原因:nvue 的 prop 同步会重复推送同一个值,且首次挂载也会触发 watch(即使 immediate: false)。

解决:必须做令牌去重(见第 4 条),且去重状态要放在能持久存活的对象里(本插件放在原生引擎实例上,不要放模块级变量,watch 处理器里读写不可靠)。

7. Kotlin 编译报错:类型不匹配 / 找不到名称

UTS 的数值类型规则(Android 端):

  • number 编译为 Kotlin Number,与原生接口的 Int / Float 不匹配,实现接口方法时必须显式声明 Int / Float
  • 循环变量、索引必须显式声明:for (let i: Int = 0; ...)
  • 数字字面量不能直接调方法:500.toLong() 会编译失败,需先赋值给变量
  • 赋值给 Int 字段的局部变量也必须显式声明类型
let i: Int = 0;                 // 正确
override onError(camera: CameraDevice, error: Int) {}   // 正确
const delayMs = 500;            // 先赋值
postDelayed(task, delayMs.toLong());                     // 再转换

8. 拍照一直卡在"处理中"

原因photo 事件没有在组件的 emits 里声明,事件被框架丢弃。

解决:UTS 组件只有声明过的事件才能正常发送

emits: ['ready', 'error', 'photo', 'debug'],

9. 预览视野偏窄 / 看到的范围比系统相机小

原因:部分机型的低分辨率预览走裁切输出,视野比全画幅窄。16:9 尺寸也是 4:3 画幅的上下裁切。

解决:优先使用 4:3 比例,必要时用更大的尺寸(如 2880x2160),或通过 size 属性现场对比。

10. iOS 端运行要求

Windows 环境或无 XCode 的 Mac 必须使用自定义基座(云端打包),Apple 开发者证书 + 真机缺一不可。Android 端因无三方依赖,标准基座即可。


更新日志

1.0.0

  • 首版发布
  • Android:Camera2 + TextureView 预览、抓帧拍照、前后摄切换、闪光灯常亮、预览尺寸自动挑选
  • iOS:AVFoundation 预览与拍照(待真机验证)
  • 提供 props / events 双向通信,规避 nvue 下 ref 不可靠问题

反馈

使用中遇到问题,请提供:

  1. 手机型号 + Android 版本
  2. HBuilderX 控制台中 [lj-costomer-camera] 开头的日志
  3. 具体现象(黑屏 / 崩溃 / 无响应 / 方向不对)

隐私、权限声明

1. 本插件需要申请的系统权限列表:

相机权限(用于预览与拍照), 闪光灯权限(用于常亮照明)

2. 本插件采集的数据、发送的服务器地址、以及数据用途说明:

本插件不采集任何数据

3. 本插件是否包含广告,如包含需详细说明广告表达方式、展示频率:

许可协议

MIT协议

暂无用户评论。