uniapp自定义相机功能

因公司业务需要,需要开发水印相机功能,而项目代码用的uniapp框架,App端只能简单调用系统的相机,无法自定义界面,在此基础上,只能开发自定义插件来完成功能(自定义原生插件,即是用原生代码来编写组件实现功能,然后供uniapp项目调用),经过半个月的研究和开发,完成了这款插件,以高度自由的形式提供了开发者相机自定义界面的需求,只需要在相机界面引入

			<!-- 相机原生插件 START -->
			<camera-view
				ref="cameraObj" 
				class="camera_view"
				:defaultCamera="currentCamera"
				@receiveRatio="receiveRatio"
				@takePhotoSuccess="takePhotoSuccess" 
				@takePhotoFail="takePhotoFail" 
				@recordSuccess="recordSuccess" 
				@recordFail="recordFail" 
				@receiveInfo="onError"
				:style="'width:'+previewWidth+'px;height:'+previewHeight+'px;margin-left:-'+marginLeft+'px'"
				>
			</camera-view>
			<!-- 相机原生插件 END -->

这里建议宽高设置为全屏,然后在界面上自定义叠加自己的按钮文字等实现自己的界面功能,然后调用插件提供的api实现物理功能

// 拍照
takePhoto(){
	console.error("开始拍照")
	// 设置水印
	this.$refs.cameraObj.addWaterText({
		"date":this.tempDateStr || "",
		"logo":"·七彩云·|水印相机",
		"address":(this.showAddress ? this.address:""),
		"time":this.tempTimeStr || "",
		"week":this.weekDay || "",
		"remark":(this.showRemark ? this.remark:"")
	});
	// 调用拍照api
	this.$refs.cameraObj.takePhoto();
},
// 切换闪光灯
	switchFlash(){
		if(this.flashStatus === 0){
			this.flashStatus = 1;
			this.$refs.cameraObj.openFlash();
		}else{
			this.flashStatus = 0;
			this.$refs.cameraObj.closeFlash();
		}
	},
// 切换摄像头
switchCamera(){
	if(this.currentCamera === "0"){
		this.currentCamera = "1";
		this.$refs.cameraObj.openFront();
	}else{
		this.currentCamera = "0";
		this.$refs.cameraObj.openBack();
	}
},	

原生插件开发文档

Android / IOS 原生插件都有两种类型扩展
1、 Module 扩展 非 UI 的特定功能. ( 直白点说就是只注重功能 )
2、 Component 扩展 实现特别功能的 Native 控件. ( 侧重点在界面 )

比如我们想实现一个自定义的原生按钮,那就得扩展Component,因为需要有界面,而想实现一个提供各种api的插件,比如加减乘除算法等不需要界面显示,只有结果数据的,这种就可以用Module

附上链接: 前往下载插件和demo实例

一、Android原生插件的实现

首先android类继承uniapp的特殊类UniComponent
public class LuanQingCamera extends UniComponent<FrameLayout>
在initComponentHostView这个固定方法返回一个组件
@Override
    protected FrameLayout initComponentHostView(Context context) {
        // 我们自定义了一个FrameLayout的组件(为了方便后面扩展水印)
        FrameLayout frameLayout = new FrameLayout(context);
        // 创建一个SurfaceView用来承载摄像头预览
        SurfaceView surfaceView = new SurfaceView(context);
        // 添加到布局中
        frameLayout.addView(surfaceView);
        
        if (mHolder == null) {
            mHolder = surfaceView.getHolder();

            mHolder.addCallback(new SurfaceHolder.Callback() {
                @Override
                public void surfaceCreated(SurfaceHolder holder) {
                    // 检查权限 如果权限满足就将打开摄像头,初始化预览
                    checkPermission();
                }

                @Override
                public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

                }

                @Override
                public void surfaceDestroyed(SurfaceHolder holder) {

                }
            });
        }

        return frameLayout;
    }
申请权限,android 6.0起需一些危险权限要动态申请,因此我们在使用摄像头前申请
    @UniJSMethod
    public void checkPermission() {
        Context mContent = mUniSDKInstance.getContext();
        if(mContent instanceof Activity){

            // 用于请求权限的列表
            List<String> permissions = new ArrayList<>();
            // 判断权限是否足够的标识变量
            boolean isEnoughPermission = true;

            // 权限检查和判断模块 START
            List<PermissionEntity> checkList = new ArrayList<>();
            checkList.add(new PermissionEntity(Manifest.permission.CAMERA,"摄像头相机权限"));
            checkList.add(new PermissionEntity(Manifest.permission.RECORD_AUDIO,"录音录制权限"));
            checkList.add(new PermissionEntity(Manifest.permission.WRITE_EXTERNAL_STORAGE,"文件读写权限"));

            for (PermissionEntity p : checkList){
                // 判断是否有权限
                boolean isHas = ActivityCompat.checkSelfPermission(mUniSDKInstance.getContext(), p.getPermissionName()) == PackageManager.PERMISSION_GRANTED;
                if (isHas) {
                    // 已经有权限(可能用户在设置中开启了)的话就把配置中的权限状态设置为已有权限
                    SharedData.setParam(mUniSDKInstance.getContext(),p.getPermissionName(),1);
                }

                // 权限状态: 0|无权限  1|有权限  2|已拒绝
                int status = (int) SharedData.getParam(mUniSDKInstance.getContext(),p.getPermissionName(),0);
                if(status == 0){
                    // 添加到权限请求列表
                    permissions.add(p.getPermissionName());
                    isEnoughPermission = false;
                }else if(status == 2){
                    isEnoughPermission = false;
                    backData("receiveInfo", 2003 ,"缺少"+p.getDescribe());
                }
            }

            // 如果权限足够了直接初始化相机
            if(isEnoughPermission){
                initCameraOption();
                return;
            }
            // 权限检查和判断模块 START

            if(permissions.size() > 0){
                EsayPermissions.with((Activity) mContent).permission(permissions).request(new OnPermission() {
                    @Override
                    public void hasPermission(List<String> granted, boolean isAll) {
                        if(isAll){
                            initCameraOption();
                        }else{
                            backData("receiveInfo", 2003 ,"缺少摄像头|录制录音|文件读写权限");
                        }
                    }

                    @Override
                    public void noPermission(List<String> denied, boolean quick) {
                        // 把已拒绝的权限记录,下次不再弹出权限申请,因为不这样做存在会被应用市场拒绝并下架的风险
                        for (String permission : denied){
                            // 用户拒绝
                            SharedData.setParam(mUniSDKInstance.getContext(),permission,2);
                        }
                        backData("receiveInfo", 2003 ,"未授予摄像头|录制录音|文件读写权限");
                    }
                });
            }
        }
    }
摄像头开始预览,显示可见的内容
// 开始预览
    @UniJSMethod
    public void startPreview() {
        try {
            if(mCameraCaptureSession != null){
                mCameraCaptureSession.stopRepeating();//停止之前的会话操作,准备切换到预览画面
                mCameraCaptureSession.close();//关闭之前的会话
                mCameraCaptureSession = null;
            }

            //创建预览请求
            mPreviewCaptureRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
            // 设置自动对焦模式
            mPreviewCaptureRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);

            //设置Surface作为预览数据的显示界面
            mPreviewCaptureRequestBuilder.addTarget(mHolder.getSurface());
            //创建相机捕获会话,第一个参数是捕获数据的输出Surface列表,第二个参数是CameraCaptureSession的状态回调接口,当它创建好后会回调onConfigured方法,第三个参数用来确定Callback在哪个线程执行,为null的话就在当前线程执行
            mCameraDevice.createCaptureSession(Arrays.asList(mHolder.getSurface(),mImageReader.getSurface()),new CameraCaptureSession.StateCallback() {
                @Override
                public void onConfigured(CameraCaptureSession session) {
                    mCameraCaptureSession = session;
                    try {
                        //开始预览
                        mPreviewCaptureRequest = mPreviewCaptureRequestBuilder.build();
                        UniLogUtils.e("初始化开启预览");
                        //设置反复捕获数据的请求,这样预览界面就会一直有数据显示
                        mCameraCaptureSession.setRepeatingRequest(mPreviewCaptureRequest, null, null);
                    } catch (CameraAccessException e) {
                        e.printStackTrace();
                    }
                }

                @Override
                public void onConfigureFailed(CameraCaptureSession session) {
                    UniLogUtils.e("预览失败");
                }
            }, null);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }
执行拍照功能
@UniJSMethod
    public void takePhoto() {
        UniLogUtils.e("准备开始拍照");
        if (mCameraDevice == null) return;
        try {
            imageFileName = System.currentTimeMillis() + ".jpg";

            //首先我们创建请求拍照的CaptureRequest
            CaptureRequest.Builder captureBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);

            Context context = mUniSDKInstance.getContext();
            if(context instanceof Activity){
                Activity activity = (Activity)mUniSDKInstance.getContext();
                //获取屏幕方向
                int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();
                //一个 CaptureRequest 除了需要配置很多参数之外,还要求至少配置一个 Surface(任何相机操作的本质都是为了捕获图像),
                captureBuilder.addTarget(mImageReader.getSurface());

                // 自动对焦
//                captureBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
//              // 自动曝光开
                captureBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON);

//                captureBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_OFF);

                // 这里有个坑,设置闪光灯必须先设置曝光
                if(flashState == 0){
                    captureBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF);
                }else{
                    captureBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_SINGLE);
                }

//                captureBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH);
//                captureBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_SINGLE);
                captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, ORIENTATIONS.get(rotation));

                mCameraCaptureSession.stopRepeating();
                CameraCaptureSession.CaptureCallback captureCallback = new CameraCaptureSession.CaptureCallback() {
                    @Override
                    public void onCaptureCompleted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) {
                        super.onCaptureCompleted(session, request, result);
                        UniLogUtils.e("拍照成功:");
                        backData("takePhotoSuccess", 200 ,"ok");
                        startPreview();
                    }

                    @Override
                    public void onCaptureFailed(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull CaptureFailure failure) {
                        super.onCaptureFailed(session, request, failure);
                        UniLogUtils.e("拍照失败:");
                        backData("takePhotoFail", 2001 ,"拍照操作失败");
                    }
                };
                UniLogUtils.e("开始拍照");

                mCameraCaptureSession.capture(captureBuilder.build(), captureCallback, null);
            }
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

二、IOS原生插件的实现

ios端相比较,更为简单

头部文件 .h
#import <AVFoundation/AVFoundation.h>
#import "DCUniComponent.h"

NS_ASSUME_NONNULL_BEGIN

@interface LQCamera : DCUniComponent

@end

NS_ASSUME_NONNULL_END
.m文件实现固定函数,并返回一个组件
- (UIView *)loadView {
    NSLog(@"插件日志:loadView");
    return [UIView new];
}
初始化一些摄像头参数
- (void)viewDidLoad {
    NSLog(@"插件日志:viewDidLoad");
    
    self.session = [[AVCaptureSession alloc] init];
    
    //创建一个AVCaptureMovieFileOutput 实例,用于将Quick Time 电影录制到文件系统
    self.movieOutput = [[AVCaptureMovieFileOutput alloc]init];
    //输出连接 判断是否可用,可用则添加到输出连接中去
    if ([self.session canAddOutput:self.movieOutput])
    {
        [self.session addOutput:self.movieOutput];
    }
    
    //     拿到的图像的大小可以自行设定
    //    AVCaptureSessionPresetHigh
    //    AVCaptureSessionPreset320x240
    //    AVCaptureSessionPreset352x288
    //    AVCaptureSessionPreset640x480
    //    AVCaptureSessionPreset960x540
    //    AVCaptureSessionPreset1280x720
    //    AVCaptureSessionPreset1920x1080
    //    AVCaptureSessionPreset3840x2160
    self.session.sessionPreset = AVCaptureSessionPreset1920x1080;
    
    //AVCaptureStillImageOutput 实例 从摄像头捕捉静态图片
    self.imageOutput = [[AVCaptureStillImageOutput alloc]init];
    //配置字典:希望捕捉到JPEG格式的图片
    self.imageOutput.outputSettings = @{AVVideoCodecKey:AVVideoCodecJPEG};
    if ([self.session canAddOutput:self.imageOutput]) {
        [self.session addOutput:self.imageOutput];
    }

    self.device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
    NSError * error = nil;
    self.input = [AVCaptureDeviceInput deviceInputWithDevice:self.device error:&error];
    
    if (self.input) {
        [self.session addInput:self.input];
    }else{
        NSLog(@"Input Error:%@",error);
    }

    //预览层的生成
    self.previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:self.session];
    
    // 直接取用本组件的bounds来做定位,因为本组件的bounds是uniapp传过来的css宽高设置过的
    self.previewLayer.frame = self.view.bounds; //预览层填充视图

    // AVLayerVideoGravityResizeAspectFill 等比例填充,直到填充满整个视图区域,其中一个维度的部分区域会被裁剪
    // AVLayerVideoGravityResize 非均匀模式。两个维度完全填充至整个视图区域
    // AVLayerVideoGravityResizeAspect 等比例填充,直到一个维度到达区域边界
    self.previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
    [self.view.layer addSublayer:self.previewLayer];

    [self.session startRunning];
}
一些固定的标注写法
/// 前端更新属性回调方法
/// @param attributes 更新的属性
- (void)updateAttributes:(NSDictionary *)attributes {
    // 解析属性
    if (attributes[@"showsTraffic"]) {
//        _showsTraffic = [DCUniConvert BOOL: attributes[@"showsTraffic"]];
    }
}

/// 前端注册的事件会调用此方法
/// @param eventName 事件名称
- (void)addEvent:(NSString *)eventName {
    if ([eventName isEqualToString:@"mapLoaded"]) {
        
    }
}

/// 对应的移除事件回调方法
/// @param eventName 事件名称
- (void)removeEvent:(NSString *)eventName {
    if ([eventName isEqualToString:@"mapLoaded"]) {
        
    }
}
ios端回调原生方法
// 返回给前端的信息回调
// 向前端发送事件,params 为传给前端的数据 注:数据最外层为 NSDictionary 格式,需要以 "detail" 作为 key 值
- (void) returnFunc:(NSString *) func returnCode:(NSNumber *)code returnMess:(NSString *) message{
    NSString *imgUrl = self.imagePath ? self.imagePath : @"";
    NSString *vioUrl = self.videoPath ? self.videoPath : @"";

    [self fireEvent:func params:@{@"detail":@{@"code":code,@"message":message,@"videoPath":vioUrl,@"imagePath":imgUrl}} domChanges:nil];
}
拍照、录像[开始、停止]、闪光灯切换、摄像头镜头切换、设置水印内容等功能接口
// 下列为暴露出来的方法列表 START
// 通过 WX_EXPORT_METHOD 将方法暴露给前端
UNI_EXPORT_METHOD(@selector(openFlash))
// 开启闪光灯
- (void)openFlash {
    [self setFlashMode:AVCaptureFlashModeOn];
}

UNI_EXPORT_METHOD(@selector(closeFlash))
// 关闭闪光灯
- (void)closeFlash {
    [self setFlashMode:AVCaptureFlashModeOff];
}

UNI_EXPORT_METHOD(@selector(autoFlash))
// 自动闪光灯
- (void)autoFlash {
    [self setFlashMode:AVCaptureFlashModeAuto];
}

UNI_EXPORT_METHOD(@selector(openFront))
// 切换前置摄像头
- (void)openFront {
    [self switchCamer:AVCaptureDevicePositionFront];
}

UNI_EXPORT_METHOD(@selector(openBack))
// 切换后置摄像头
- (void)openBack {
    [self switchCamer:AVCaptureDevicePositionBack];
}

// 通过 WX_EXPORT_METHOD 将方法暴露给前端
UNI_EXPORT_METHOD(@selector(takePhoto:))
// 拍照
- (void)takePhoto:(NSDictionary *)options {
    // options 为前端传递的参数
    NSLog(@"IOS收到开始拍照请求");
    
    //获取连接
    AVCaptureConnection *connection = [self.imageOutput connectionWithMediaType:AVMediaTypeVideo];

    //程序只支持纵向,但是如果用户横向拍照时,需要调整结果照片的方向
    //判断是否支持设置视频方向
    if (connection.isVideoOrientationSupported) {
        
        //获取方向值
        connection.videoOrientation = [self currentVideoOrientation];
    }

    //定义一个handler 块,会返回1个图片的NSData数据
    id handler = ^(CMSampleBufferRef sampleBuffer,NSError *error)
                {
                    if (sampleBuffer != NULL) {
                        NSData *imageData = [AVCaptureStillImageOutput jpegStillImageNSDataRepresentation:sampleBuffer];
                        UIImage *image = [[UIImage alloc]initWithData:imageData];
                        
                        [self returnFunc:@"takePhotoSuccess" returnCode:@200 returnMess:@"拍照成功"];
                        //重点:捕捉图片成功后,将图片传递出去
                        [self saveImage:image];
                    }else
                    {
                        NSLog(@"保存出错NULL sampleBuffer:%@",[error localizedDescription]);
                    }
                };
    
    //捕捉静态图片
    [self.imageOutput captureStillImageAsynchronouslyFromConnection:connection completionHandler:handler];
}

UNI_EXPORT_METHOD(@selector(addWaterText:))
// 添加水印
- (void)addWaterText:(NSDictionary *)options{
    NSLog(@"接收到水印内容:%@",options);
    
    if(options[@"time"]){
        self.timeStr = options[@"time"];
    }
    if(options[@"date"]){
        self.dateStr = options[@"date"];
    }
    if(options[@"week"]){
        self.weekStr = options[@"week"];
    }
    if(options[@"address"]){
        self.addressStr = options[@"address"];
    }
    if(options[@"remark"]){
        self.remarkStr = options[@"remark"];
    }
    if(options[@"logo"]){
        self.logoStr = options[@"logo"];
    }
}

// 停止录制
UNI_EXPORT_METHOD(@selector(stopRecord))
- (void)stopRecord {
    NSLog(@"停止录像");
    [self.movieOutput stopRecording];
}

// 开始录制
UNI_EXPORT_METHOD(@selector(startRecord))
- (void)startRecord {
        NSLog(@"开始录像");

        // 获取当前视频捕捉连接信息,用于捕捉视频数据配置一些核心属性
        AVCaptureConnection * videoConnection = [self.movieOutput connectionWithMediaType:AVMediaTypeVideo];
        
        //判断是否支持设置videoOrientation 属性。
        if([videoConnection isVideoOrientationSupported])
        {
            //支持则修改当前视频的方向
            videoConnection.videoOrientation = [self currentVideoOrientation];
            
        }
        
        //判断是否支持视频稳定 可以显著提高视频的质量。只会在录制视频文件涉及
        if([videoConnection isVideoStabilizationSupported])
        {
            videoConnection.enablesVideoStabilizationWhenAvailable = YES;
        }
        
        
        AVCaptureDevice *device = self.input.device;
        
        //摄像头可以进行平滑对焦模式操作。即减慢摄像头镜头对焦速度。当用户移动拍摄时摄像头会尝试快速自动对焦。
        if (device.isSmoothAutoFocusEnabled) {
            NSError *error;
            if ([device lockForConfiguration:&error]) {
                
                device.smoothAutoFocusEnabled = YES;
                [device unlockForConfiguration];
            }else
            {
//                [self.delegate deviceConfigurationFailedWithError:error];
            }
        }
        
        //查找写入捕捉视频的唯一文件系统URL.
//        self.outputURL = [self uniqueURL];
        NSLog(@"开始录像2");

        //在捕捉输出上调用方法 参数1:录制保存路径  参数2:代理
        [self.movieOutput startRecordingToOutputFileURL:[self outPutFileURL] recordingDelegate:self];
}
// 下列为暴露出来的方法列表 END

到此一款包含Android+IOS两端的Uniapp原生插件完成

附上链接: 前往下载插件和demo实例

效果图:
在这里插入图片描述

  • 5
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
本课程讲了Vue3+Vue2+Uni-app(uniapp)入门基础与实战,其中还重点讲解了ES6、TypeScript这些基础知识,实战由两大价值5000元的真实企业级项目组成,分别是仿京东电商网站和仿美团微信点餐小程序,同时两大项目代码全部赠送,还赠送前后端架构模板,工作中直接使用。VUE和uni-app是目前热门的前端框架,本课程教你如何快速学会VUE和uni-app并应用到实战,教你如何解决内存泄漏,常用UI库的使用,自己封装组件和插件,正式上线白屏问题,性能优化、解决iphoneX“刘海”兼容性问题、微信支付、微信授权登录,获取位置在地图上显示,获取用户所在的城市和街道信息,微信小程序发布审核等。对正在工作当中或打算学习VUE和uni-app高薪就业的你来说,那么这门课程便是你手中的葵花宝典。学习技巧:学习当中不要只看,一定要多敲代码,如果碰到某一个知识点不是很明白,不要钻牛角尖,千万不要因为一个点,放弃整个森林,接着往下学,硬着头皮开发项目。只要能亲自开发一个完整的项目,你会发现不明白的地方自然而然就明白了,项目做出来就真正的学会了。此vue和uni-app课程以面试和实战为基础进行讲解,每个知识点都会让你知道在实际项目开发中如何使用,学习后,可以开发大型项目,增强逻辑思维,至少让你拥有3年以上开发经验的实力!代码和ppt均可下载!免费提供《企业级完整实战项目接口文档》,绝对可用
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值