這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 因公司業務需要,需要開發水印相機功能,而項目代碼用的uniapp框架,App端只能簡單調用系統的相機,無法自定義界面,在此基礎上,只能開發自定義插件來完成功能(自定義原生插件,即是用原生代碼來編寫組件實現功能,然後供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實例
效果圖: