当前位置:

React Native 实现截图添加二维码功能

访客 2024-04-23 619 0

截图捕捉功能已经发布到NPM,欢迎使用Android10ContentObserver经常失效,暂时还没找到新的处理方式,欢迎大家提改进意见npmireact-native-screenshotcatch

对一个JSer来说,用原生来实现一个功能着实不容易。但是,随着APP开发的深入,在许多场景下RN现成的组件已经不能满足我们的需求,不想受制于人就要自己动手。图像绘制、文件系统、通知、模块封装等等,虽然难但是收获也多,希望自己能够更深入原生开发的领域。

效果展示截屏监听功能iOS截屏监听实现

实现思路:添加iOS自带的UIApplicationUserDidTakeScreenshotNotification通知监听,捕捉到事件后绘制当前页面,保存返回文件地址

//ScreenShotShare.h#import<React/RCTBridgeModule.h>#import<React/RCTEventEmitter.h>@interfaceScreenShotShare:RCTEventEmitter<RCTBridgeModule>@end//ScreenShotShare.m#import"ScreenShotShare.h"#import<React/RCTConvert.h>#import<React/RCTEventDispatcher.h>#definePATH@"screen-shot-share"@implementationScreenShotShareRCT_EXPORT_MODULE();-(NSArray<NSString*>*)supportedEvents{return@[@"ScreenShotShare"];}RCT_EXPORT_METHOD(startListener){[selfaddScreenShotObserver];}-(void)addScreenShotObserver{[[NSNotificationCenterdefaultCenter]addObserver:selfselector:@selector(getScreenShot:)name:UIApplicationUserDidTakeScreenshotNotificationobject:nil];}-(void)removeScreenShotObserver{[[NSNotificationCenterdefaultCenter]removeObserver:selfname:UIApplicationUserDidTakeScreenshotNotificationobject:nil];}-(void)getScreenShot:(NSNotification*)notification{[selfsendEventWithName:@"ScreenShotShare"body:[selfscreenImage]];}//保存文件并返回文件路径-(NSDictionary*)screenImage{@try{UIImage*image=[UIImageimageWithData:[selfimageDataScreenShot]];NSArray*paths=NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES);NSFileManager*fileManager=[NSFileManagerdefaultManager];NSString*path=[[pathsobjectAtIndex:0]stringByAppendingPathComponent:PATH];if(![fileManagerfileExistsAtPath:path]){[fileManagercreateDirectoryAtPath:pathwithIntermediateDirectories:YESattributes:nilerror:nil];}longtime=(long)[[NSDatenew]timeIntervalSince1970];NSString*filePath=[pathstringByAppendingPathComponent:[NSStringstringWithFormat:@"screen-capture-%ld.png",time]];@try{BOOLresult=[UIImagePNGRepresentation(image)writeToFile:filePathatomically:YES];//保存成功会返回YESif(result==YES){NSLog(@"agan_app保存成功。filePath:%@",filePath);[[[UIApplicationsharedApplication]keyWindow]endEditing:YES];//获取截屏后关闭键盘return@{@"code":@200,@"uri":filePath};}}@catch(NSException*ex){NSLog(@"agan_app保存图片失败:%@",ex.description);filePath=@"";return@{@"code":@500,@"errMsg":@"保存图片失败"};}}@catch(NSException*ex){NSLog(@"agan_app截屏失败:%@",ex.description);return@{@"code":@500,@"errMsg":@"截屏失败"};}}//截屏-(NSData*)imageDataScreenShot{CGSizeimageSize=[UIScreenmainScreen].bounds.size;UIGraphicsBeginImageContextWithOptions(imageSize,NO,0);CGContextRefcontext=UIGraphicsGetCurrentContext();for(UIWindow*windowin[[UIApplicationsharedApplication]windows]){CGContextSaveGState(context);CGContextTranslateCTM(context,window.center.x,window.center.y);CGContextConcatCTM(context,window.transform);CGContextTranslateCTM(context,-window.bounds.size.width*window.layer.anchorPoint.x,-window.bounds.size.height*window.layer.anchorPoint.y);if([windowrespondsToSelector:@selector(drawViewHierarchyInRect:afterScreenUpdates:)]){NSLog(@"agan_app使用drawViewHierarchyInRect:afterScreenUpdates:");[windowdrawViewHierarchyInRect:window.boundsafterScreenUpdates:YES];}else{NSLog(@"agan_app使用renderInContext:");[window.layerrenderInContext:context];}CGContextRestoreGState(context);}UIImage*image=UIGraphicsGetImageFromCurrentImageContext();UIGraphicsEndImageContext();returnUIImagePNGRepresentation(image);}@endAndroid截屏监听实现

实现思路:通过ContentObserver获取文件变化捕获截图事件,捕获后为了去掉状态栏以及虚拟导航栏使用normalShot方法自己绘制当前页面然后保存,返回文件路径。

//ScreenShotSharePackage.javapublicclassScreenShotShareModuleextendsReactContextBaseJavaModule{privatestaticfinalStringTAG="screenshotshare";privatestaticfinalStringNAVIGATION="navigationBarBackground";privatestaticfinalString[]KEYWORDS={"screenshot","screen_shot","screen-shot","screenshot","screencapture","screen_capture","screen-capture","screencapture","screencap","screen_cap","screen-cap","screencap","截屏","截图"};privatestaticActivityma;privateReactContextreactContext;/**读取媒体数据库时需要读取的列*/privatestaticfinalString[]MEDIA_PROJECTIONS={MediaStore.Images.ImageColumns.DATA,MediaStore.Images.ImageColumns.DATE_TAKEN,};/**内部存储器内容观察者*/privateContentObservermInternalObserver;/**外部存储器内容观察者*/privateContentObservermExternalObserver;privateHandlerThreadmHandlerThread;privateHandlermHandler;publicScreenShotShareModule(ReactApplicationContextreContext){super(reContext);this.reactContext=reContext;}@OverridepublicStringgetName(){return"ScreenShotShare";}publicstaticvoidinitScreenShotShareSDK(Activityactivity){ma=activity;}@ReactMethodpublicvoidstartListener(){mHandlerThread=newHandlerThread("Screenshot_Observer");mHandlerThread.start();mHandler=newHandler(mHandlerThread.getLooper());//初始化mInternalObserver=newMediaContentObserver(MediaStore.Images.Media.INTERNAL_CONTENT_URI,mHandler);mExternalObserver=newMediaContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,mHandler);//添加监听this.reactContext.getContentResolver().registerContentObserver(MediaStore.Images.Media.INTERNAL_CONTENT_URI,false,mInternalObserver);this.reactContext.getContentResolver().registerContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,false,mExternalObserver);}@ReactMethodpublicvoidstopListener(){this.reactContext.getContentResolver().unregisterContentObserver(mInternalObserver);this.reactContext.getContentResolver().unregisterContentObserver(mExternalObserver);}@ReactMethodpublicvoidhasNavigationBar(Promisepromise){booleannavigationBarExisted=isNavigationBarExist(ma);promise.resolve(navigationBarExisted);}privatevoidhandleMediaContentChange(UricontentUri){Cursorcursor=null;try{//数据改变时查询数据库中最后加入的一条数据cursor=this.reactContext.getContentResolver().query(contentUri,MEDIA_PROJECTIONS,null,null,MediaStore.Images.ImageColumns.DATE_ADDED"desclimit1");if(cursor==null){return;}if(!cursor.moveToFirst()){return;}//获取各列的索引intdataIndex=cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA);intdateTakenIndex=cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATE_TAKEN);//获取行数据Stringdata=cursor.getString(dataIndex);longdateTaken=cursor.getLong(dateTakenIndex);//处理获取到的第一行数据handleMediaRowData(data,dateTaken);}catch(Exceptione){WritableMapmap=Arguments.createMap();map.putInt("code",500);sendEvent(this.reactContext,"ScreenShotShare",map);e.printStackTrace();}finally{if(cursor!=null&&!cursor.isClosed()){cursor.close();}}}/***处理监听到的资源*/privatevoidhandleMediaRowData(Stringdata,longdateTaken){if(checkScreenShot(data,dateTaken)){Log.d(TAG,data""dateTaken);saveBitmap(normalShot(ma));}else{Log.d(TAG,"Notscreenshotevent");WritableMapmap=Arguments.createMap();map.putInt("code",500);sendEvent(this.reactContext,"ScreenShotShare",map);}}/***判断是否是截屏*/privatebooleancheckScreenShot(Stringdata,longdateTaken){data=data.toLowerCase();//判断图片路径是否含有指定的关键字之一,如果有,则认为当前截屏了for(StringkeyWork:KEYWORDS){if(data.contains(keyWork)){returntrue;}}returnfalse;}privateclassMediaContentObserverextendsContentObserver{privateUrimContentUri;publicMediaContentObserver(UricontentUri,Handlerhandler){super(handler);mContentUri=contentUri;}@OverridepublicvoidonChange(booleanselfChange){super.onChange(selfChange);Log.d(TAG,mContentUri.toString());handleMediaContentChange(mContentUri);}}publicvoidsendEvent(ReactContextreactContext,StringeventName,@NullableWritableMapparams){reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit(eventName,params);}//判断全面屏虚拟导航栏是否存在publicstaticbooleanisNavigationBarExist(Activityactivity){ViewGroupvp=(ViewGroup)activity.getWindow().getDecorView();if(vp!=null){for(inti=0;i<vp.getChildCount();i){vp.getChildAt(i).getContext().getPackageName();if(vp.getChildAt(i).getId()!=NO_ID&&NAVIGATION.equals(activity.getResources().getResourceEntryName(vp.getChildAt(i).getId()))){returntrue;}}}returnfalse;}//当前APP内容截图privatestaticBitmapnormalShot(Activityactivity){ViewdecorView=activity.getWindow().getDecorView();decorView.setDrawingCacheEnabled(true);decorView.buildDrawingCache();RectoutRect=newRect();decorView.getWindowVisibleDisplayFrame(outRect);intstatusBarHeight=outRect.top;//状态栏高度Bitmapbitmap=Bitmap.createBitmap(decorView.getDrawingCache(),0,statusBarHeight,decorView.getMeasuredWidth(),decorView.getMeasuredHeight()-statusBarHeight);decorView.setDrawingCacheEnabled(false);decorView.destroyDrawingCache();returnbitmap;}//获取当前APP图片存储路径privateStringgetSystemFilePath(){StringcachePath;if(Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())||!Environment.isExternalStorageRemovable()){cachePath=reactContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES).getAbsolutePath();//cachePath=context.getExternalCacheDir().getPath();//返回文件uri,而非path}else{cachePath=reactContext.getFilesDir().getAbsolutePath();//cachePath=context.getCacheDir().getPath();//返回文件uri,而非path}returncachePath;}//保存截屏的bitmap为图片文件并返回路径privatevoidsaveBitmap(Bitmapbitmap){Longtime=System.currentTimeMillis();Stringpath=getSystemFilePath()"/screen-capture-"time".png";Log.d(TAG,path);FilefilePic;WritableMapmap=Arguments.createMap();try{filePic=newFile(path);if(!filePic.exists()){filePic.getParentFile().mkdirs();filePic.createNewFile();}FileOutputStreamfos=newFileOutputStream(filePic);bitmap.compress(Bitmap.CompressFormat.PNG,100,fos);fos.flush();fos.close();map.putInt("code",200);map.putString("uri",filePic.getAbsolutePath());sendEvent(this.reactContext,"ScreenShotShare",map);//强制关闭软键盘((InputMethodManager)ma.getSystemService(reactContext.INPUT_METHOD_SERVICE)).hideSoftInputFromWindow(ma.getCurrentFocus().getWindowToken(),InputMethodManager.HIDE_NOT_ALWAYS);}catch(IOExceptione){e.printStackTrace();map.putInt("code",500);sendEvent(this.reactContext,"ScreenShotShare",map);}}}分享功能

我集成了Umeng的shareSDK,但是没有现成的纯图片分享接口,需要自己封装

iOS//UMShareModule.m中自定义shareImageRCT_EXPORT_METHOD(shareImage:(NSString*)urlicon:(NSString*)iconplatform:(NSInteger)platformcompletion:(RCTResponseSenderBlock)completion){UMSocialPlatformTypeplf=[selfplatformType:platform];if(plf==UMSocialPlatformType_UnKnown){if(completion){completion(@[@(UMSocialPlatformType_UnKnown),@"invalidplatform"]);return;}}UIImage*image=[UIImageimageWithContentsOfFile:url];//创建分享消息对象UMSocialMessageObject*messageObject=[UMSocialMessageObjectmessageObject];//创建图片内容对象UMShareImageObject*shareObject=[[UMShareImageObjectalloc]init];//如果有缩略图,则设置缩略图shareObject.thumbImage=[UIImageimageNamed:icon];[shareObjectsetShareImage:image];//分享消息对象设置分享内容对象messageObject.shareObject=shareObject;//调用分享接口[[UMSocialManagerdefaultManager]shareToPlatform:plfmessageObject:messageObjectcurrentViewController:nilcompletion:^(iddata,NSError*error){if(error){NSLog(@"appppp%@",error);if(completion){completion(@[@-1,error]);}}else{if(completion){completion(@[@200,data]);}}}];}Android//ShareModule.java中自定义shareImage@ReactMethodpublicvoidshareImage(finalStringurl,finalStringicon,finalintsharemedia,finalCallbacksuccessCallback){runOnMainThread(newRunnable(){@Overridepublicvoidrun(){Uriuri=Uri.parse(url);FileimageFile=newFile(getPath(contect,uri));UMImageimage=newUMImage(ma,imageFile);newShareAction(ma).withMedia(image).setPlatform(getShareMedia(sharemedia)).setCallback(getUMShareListener(successCallback)).share();}});}//uri转pathprivateStringgetPath(Contextcontext,Uriuri){String[]projection={MediaStore.Video.Media.DATA};Cursorcursor=context.getContentResolver().query(uri,projection,null,null,null);intcolumn_index=cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA);cursor.moveToFirst();returncursor.getString(column_index);}封装调用封装//ScreenShotShareUtil.jsimport{NativeModules,NativeEventEmitter,DeviceEventEmitter}from'react-native'letscreenCaptureEmitter=undefinedexportdefaultclassScreenShotShareUtil{staticstartListener(callback){constScreenShotShare=NativeModules.ScreenShotSharescreenCaptureEmitter&&screenCaptureEmitter.removeAllListeners('ScreenShotShare')screenCaptureEmitter=Adapter.isIOS?newNativeEventEmitter(ScreenShotShare):DeviceEventEmitterscreenCaptureEmitter.addListener('ScreenShotShare',(data)=>{if(callback){callback(data)}})ScreenShotShare.startListener()returnscreenCaptureEmitter}staticstopListener(){screenCaptureEmitter&&screenCaptureEmitter.removeAllListeners('ScreenShotShare')constscreenCaptureEmitter=NativeModules.ScreenShotSharereturnscreenCaptureEmitter.stopListener()}statichasNavigationBar(){if(!Adapter.isIOS){screenCaptureEmitter&&screenCaptureEmitter.removeAllListeners('ScreenShotShare')constscreenCaptureEmitter=NativeModules.ScreenShotSharereturnscreenCaptureEmitter.hasNavigationBar()}else{returnfalse}}}//ShareUtil.js//分享图片exportconstshareImage=(url,platform)=>{platform=platform||'weixin'letpl_int=2switch(platform){case'weixin':pl_int=2breakcase'timeline':pl_int=3breakcase'qq':pl_int=0breakcase'qzone':pl_int=4breakcase'weibo':pl_int=1breakdefault:pl_int=2break}returnnewPromise((resolve,reject)=>{UMShare.shareImage(url,IMAGE_URL,pl_int,(code,message)=>{if(__DEV__){console.log(`分享图片到${platform}`,code,message)}})})}调用//index.jsimportScreenShotShareModalfrom'./ScreenShotShareModal'import{ToastComponent}from'react-native-pickers'//...componentWillMount(){ScreenShotShareUtil.startListener(res=>{if(res&&res.code===200){this.screenShotShareModal.show(res.uri)}else{ToastComponent.show('获取截图失败');}})}componentWillUnmount(){ScreenShotShareUtil.stopListener()}render(){return(<Viewstyle={{flex:1,backgroundColor:'#fff',justifiContent:'center',alignItems:'center'}}><Text>...</Text><ScreenShotShareModalref={ref=>this.screenShotShareModal=ref}/></View>)}//...//ScreenShotShareModal.jsimport{BaseDialog}from'react-native-pickers'import{shareImage}from'./ShareUtil'importQRCodefrom'react-native-qrcode-svg'importViewShotfrom'react-native-view-shot'exportdefaultclassScreenShotShareModalextendsBaseDialog{constructor(props){super(props)this.state={image:null,logoUri:'base64://xxxxx',text:'xxx'}this.viewShot=React.createRef()}show(uri){this.setState({image:uri},()=>{super.show()})}renderContent(){return(<View><View><ViewShotref={this.viewShot}><View><Imagesource={{uri:(isIOS?this.state.image:`file://${this.state.image}`)}}/><QRCodevalue={this.state.text}size={60}logo={{uri:this.state.logoUri}}logoSize={15}logoBackgroundColor='white'logoBorderRadius={3}/><Text>扫描二维码下载《XXX》</Text></View></ViewShot></View><View><Text>分享至</Text><Viewstyle={styles.itemGroup}><Viewstyle={styles.item}><TouchableOpacityactiveOpacity={0.9}onPress={()=>this._shareImage('weixin')}><Text>微信</Text></TouchableOpacity></View><Viewstyle={styles.item}><TouchableOpacityactiveOpacity={0.9}onPress={()=>this._shareImage('timeline')}><Text>朋友圈</Text></TouchableOpacity></View><Viewstyle={styles.item}><TouchableOpacityactiveOpacity={0.9}onPress={()=>this._shareImage('weibo')}><Text>微博</Text></TouchableOpacity></View><Viewstyle={styles.item}><TouchableOpacityactiveOpacity={0.9}onPress={()=>this._shareImage('qq')}><Text>QQ</Text></TouchableOpacity></View><Viewstyle={styles.item}><TouchableOpacityactiveOpacity={0.9}onPress={()=>this._shareImage('qzone')}><Text>空间</Text></TouchableOpacity></View></View></View></View>)}_shareImage(plf){this.viewShot.current.capture().then(imageUri=>{if(!isIOS){CameraRoll.saveToCameraRoll(imageUri).then(res=>{if(res){shareImage(res,plf)}}).catch(err=>{if(__DEV__){console.log(err)}})}else{shareImage(imageUri,plf)}})}}conststyles=StyleSheet.create({itemGroup:{flexDirection:'row',justifyContent:'space-between',padding:15},item:{justifyContent:'center',alignItems:'center',flexDirection:'column'}})参考文章
  • UmengShare文档
  • react-native-lewin-screen-capture
iOS
  • iOS捕捉截屏事件并展示截图
  • 【ReactNative】与iOS组件间的相互调用
  • IOS原生模块向ReactNative发送事件消息
  • IOS本地图片加载
Android
  • Android截屏监听(截图分享功能实现)
  • Android截屏事件监听
  • ReactNative之Android原生通过DeviceEventEmitter发送消息给js
  • reactnative中的ReadableMap和WritableMap的使用
  • Android--超全的File,Bitmap,Drawable,Uri,FilePath,byte[]之间的转换方法
  • Android之uri、file、path相互转化
  • Android开发managedQuery方法过时如何解决
  • 将bitmap对象保存到本地,返回保存的图片路径
  • Android普通截屏(不包括状态栏内容无状态栏占位仅包含应用程序)

发表评论

  • 评论列表
还没有人评论,快来抢沙发吧~