内容简介:对一个 JSer 来说,用原生来实现一个功能着实不容易。但是,随着APP开发的深入,在许多场景下RN现成的组件已经不能满足我们的需求,不想受制于人就要自己动手。图像绘制、文件系统、通知、模块封装等等,虽然难但是收获也多,希望自己能够更深入原生开发的领域。实现思路:添加iOS自带的UIApplicationUserDidTakeScreenshotNotification通知监听,捕捉到事件后绘制当前页面,保存返回文件地址
原文地址:http://liu-hang.cn/2019/06/11/185-react-native-screen-shot-share/
对一个 JSer 来说,用原生来实现一个功能着实不容易。但是,随着APP开发的深入,在许多场景下RN现成的组件已经不能满足我们的需求,不想受制于人就要自己动手。图像绘制、文件系统、通知、模块封装等等,虽然难但是收获也多,希望自己能够更深入原生开发的领域。
效果展示
截屏监听功能
iOS 截屏监听实现
实现思路:添加iOS自带的UIApplicationUserDidTakeScreenshotNotification通知监听,捕捉到事件后绘制当前页面,保存返回文件地址
// ScreenShotShare.h #import <React/RCTBridgeModule.h> #import <React/RCTEventEmitter.h> @interface ScreenShotShare : RCTEventEmitter <RCTBridgeModule> @end // ScreenShotShare.m #import "ScreenShotShare.h" #import <React/RCTConvert.h> #import <React/RCTEventDispatcher.h> #define PATH @"screen-shot-share" @implementation ScreenShotShare RCT_EXPORT_MODULE(); - (NSArray <NSString *> *)supportedEvents{ return @[@"ScreenShotShare"]; } RCT_EXPORT_METHOD(startListener){ [self addScreenShotObserver]; } - (void)addScreenShotObserver{ [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(getScreenShot:) name:UIApplicationUserDidTakeScreenshotNotification object:nil]; } - (void)removeScreenShotObserver{ [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationUserDidTakeScreenshotNotification object:nil]; } - (void)getScreenShot:(NSNotification *)notification{ [self sendEventWithName:@"ScreenShotShare" body:[self screenImage]]; } // 保存文件并返回文件路径 - (NSDictionary *)screenImage{ @try{ UIImage *image = [UIImage imageWithData: [self imageDataScreenShot]]; NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSFileManager *fileManager = [NSFileManager defaultManager]; NSString *path =[[paths objectAtIndex:0]stringByAppendingPathComponent:PATH]; if (![fileManager fileExistsAtPath:path]) { [fileManager createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:nil]; } long time = (long)[[NSDate new] timeIntervalSince1970]; NSString *filePath = [path stringByAppendingPathComponent: [NSString stringWithFormat:@"screen-capture-%ld.png", time]]; @try{ BOOL result = [UIImagePNGRepresentation(image) writeToFile:filePath atomically:YES]; // 保存成功会返回YES if (result == YES) { NSLog(@"agan_app 保存成功。filePath:%@", filePath); [[[UIApplication sharedApplication] 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{ CGSize imageSize = [UIScreen mainScreen].bounds.size; UIGraphicsBeginImageContextWithOptions(imageSize, NO, 0); CGContextRef context = UIGraphicsGetCurrentContext(); for(UIWindow *window in [[UIApplication sharedApplication] 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 ([window respondsToSelector:@selector(drawViewHierarchyInRect:afterScreenUpdates:)]){ NSLog(@"agan_app 使用drawViewHierarchyInRect:afterScreenUpdates:"); [window drawViewHierarchyInRect:window.bounds afterScreenUpdates:YES]; }else{ NSLog(@"agan_app 使用renderInContext:"); [window.layer renderInContext:context]; } CGContextRestoreGState(context); } UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return UIImagePNGRepresentation(image); } @end 复制代码
Android 截屏监听实现
实现思路:通过 ContentObserver 获取文件变化捕获截图事件,捕获后为了去掉状态栏以及虚拟导航栏使用 normalShot 方法自己绘制当前页面然后保存,返回文件路径。
// ScreenShotSharePackage.java public class ScreenShotShareModule extends ReactContextBaseJavaModule { private static final String TAG = "screenshotshare"; private static final String NAVIGATION= "navigationBarBackground"; private static final String[] KEYWORDS = { "screenshot", "screen_shot", "screen-shot", "screen shot", "screencapture", "screen_capture", "screen-capture", "screen capture", "screencap", "screen_cap", "screen-cap", "screen cap" }; private static Activity ma; private ReactContext reactContext; /** 读取媒体数据库时需要读取的列 */ private static final String[] MEDIA_PROJECTIONS = { MediaStore.Images.ImageColumns.DATA, MediaStore.Images.ImageColumns.DATE_TAKEN, }; /** 内部存储器内容观察者 */ private ContentObserver mInternalObserver; /** 外部存储器内容观察者 */ private ContentObserver mExternalObserver; private HandlerThread mHandlerThread; private Handler mHandler; public ScreenShotShareModule(ReactApplicationContext reContext){ super(reContext); this.reactContext = reContext; } @Override public String getName() { return "ScreenShotShare"; } public static void initScreenShotShareSDK(Activity activity){ ma = activity; } @ReactMethod public void startListener(){ mHandlerThread = new HandlerThread("Screenshot_Observer"); mHandlerThread.start(); mHandler = new Handler(mHandlerThread.getLooper()); // 初始化 mInternalObserver = new MediaContentObserver(MediaStore.Images.Media.INTERNAL_CONTENT_URI, mHandler); mExternalObserver = new MediaContentObserver(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 ); } @ReactMethod public void stopListener(){ this.reactContext.getContentResolver().unregisterContentObserver(mInternalObserver); this.reactContext.getContentResolver().unregisterContentObserver(mExternalObserver); } @ReactMethod public void hasNavigationBar(Promise promise){ boolean navigationBarExisted = isNavigationBarExist(ma); promise.resolve(navigationBarExisted); } private void handleMediaContentChange(Uri contentUri) { Cursor cursor = null; try { // 数据改变时查询数据库中最后加入的一条数据 cursor = this.reactContext.getContentResolver().query( contentUri, MEDIA_PROJECTIONS, null, null, MediaStore.Images.ImageColumns.DATE_ADDED + " desc limit 1" ); if (cursor == null) { return; } if (!cursor.moveToFirst()) { return; } // 获取各列的索引 int dataIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA); int dateTakenIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATE_TAKEN); // 获取行数据 String data = cursor.getString(dataIndex); long dateTaken = cursor.getLong(dateTakenIndex); // 处理获取到的第一行数据 handleMediaRowData(data, dateTaken); } catch (Exception e) { WritableMap map = Arguments.createMap(); map.putInt("code", 500); sendEvent(this.reactContext, "ScreenShotShare", map); e.printStackTrace(); } finally { if (cursor != null && !cursor.isClosed()) { cursor.close(); } } } /** * 处理监听到的资源 */ private void handleMediaRowData(String data, long dateTaken) { if (checkScreenShot(data, dateTaken)) { Log.d(TAG, data + " " + dateTaken); saveBitmap(normalShot(ma)); } else { Log.d(TAG, "Not screenshot event"); WritableMap map = Arguments.createMap(); map.putInt("code", 500); sendEvent(this.reactContext, "ScreenShotShare", map); } } /** * 判断是否是截屏 */ private boolean checkScreenShot(String data, long dateTaken) { data = data.toLowerCase(); // 判断图片路径是否含有指定的关键字之一, 如果有, 则认为当前截屏了 for (String keyWork : KEYWORDS) { if (data.contains(keyWork)) { return true; } } return false; } private class MediaContentObserver extends ContentObserver { private Uri mContentUri; public MediaContentObserver(Uri contentUri, Handler handler) { super(handler); mContentUri = contentUri; } @Override public void onChange(boolean selfChange) { super.onChange(selfChange); Log.d(TAG, mContentUri.toString()); handleMediaContentChange(mContentUri); } } public void sendEvent(ReactContext reactContext, String eventName, @Nullable WritableMap params) { reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit(eventName, params); } // 判断全面屏虚拟导航栏是否存在 public static boolean isNavigationBarExist(Activity activity){ ViewGroup vp = (ViewGroup) activity.getWindow().getDecorView(); if (vp != null) { for (int i = 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()))) { return true; } } } return false; } // 当前APP内容截图 private static Bitmap normalShot(Activity activity) { View decorView = activity.getWindow().getDecorView(); decorView.setDrawingCacheEnabled(true); decorView.buildDrawingCache(); Rect outRect = new Rect(); decorView.getWindowVisibleDisplayFrame(outRect); int statusBarHeight = outRect.top;//状态栏高度 Bitmap bitmap = Bitmap.createBitmap(decorView.getDrawingCache(), 0, statusBarHeight, decorView.getMeasuredWidth(), decorView.getMeasuredHeight() - statusBarHeight); decorView.setDrawingCacheEnabled(false); decorView.destroyDrawingCache(); return bitmap; } // 获取当前APP图片存储路径 private String getSystemFilePath() { String cachePath; 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 } return cachePath; } // 保存截屏的bitmap为图片文件并返回路径 private void saveBitmap(Bitmap bitmap){ Long time = System.currentTimeMillis(); String path = getSystemFilePath() + "/screen-capture-" + time + ".png"; Log.d(TAG, path); File filePic; WritableMap map = Arguments.createMap(); try{ filePic = new File(path); if (!filePic.exists()) { filePic.getParentFile().mkdirs(); filePic.createNewFile(); } FileOutputStream fos = new FileOutputStream(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(IOException e){ e.printStackTrace(); map.putInt("code", 500); sendEvent(this.reactContext, "ScreenShotShare", map); } } } 复制代码
分享功能
我集成了Umeng的share SDK,但是没有现成的纯图片分享接口,需要自己封装
iOS
// UMShareModule.m 中自定义 shareImage RCT_EXPORT_METHOD(shareImage:(NSString *)url icon:(NSString *)icon platform:(NSInteger)platform completion:(RCTResponseSenderBlock)completion){ UMSocialPlatformType plf = [self platformType:platform]; if (plf == UMSocialPlatformType_UnKnown) { if (completion) { completion(@[@(UMSocialPlatformType_UnKnown), @"invalid platform"]); return; } } UIImage *image = [UIImage imageWithContentsOfFile:url]; //创建分享消息对象 UMSocialMessageObject *messageObject = [UMSocialMessageObject messageObject]; //创建图片内容对象 UMShareImageObject *shareObject = [[UMShareImageObject alloc] init]; //如果有缩略图,则设置缩略图 shareObject.thumbImage = [UIImage imageNamed:icon]; [shareObject setShareImage:image]; //分享消息对象设置分享内容对象 messageObject.shareObject = shareObject; //调用分享接口 [[UMSocialManager defaultManager] shareToPlatform:plf messageObject:messageObject currentViewController:nil completion:^(id data, NSError *error) { if (error) { NSLog(@"appppp %@", error); if (completion) { completion(@[@-1, error]); } }else{ if (completion) { completion(@[@200, data]); } } }]; } 复制代码
Android
// ShareModule.java 中自定义 shareImage @ReactMethod public void shareImage(final String url, final String icon, final int sharemedia, final Callback successCallback){ runOnMainThread(new Runnable() { @Override public void run() { Uri uri = Uri.parse(url); File imageFile = new File(getPath(contect, uri)); UMImage image = new UMImage(ma, imageFile); new ShareAction(ma) .withMedia(image) .setPlatform(getShareMedia(sharemedia)) .setCallback(getUMShareListener(successCallback)) .share(); } }); } // uri 转 path private String getPath(Context context, Uri uri) { String[] projection = {MediaStore.Video.Media.DATA}; Cursor cursor = context.getContentResolver().query(uri, projection, null, null, null); int column_index = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA); cursor.moveToFirst(); return cursor.getString(column_index); } 复制代码
封装调用
封装
// ScreenShotShareUtil.js import { NativeModules, NativeEventEmitter, DeviceEventEmitter } from 'react-native' let screenCaptureEmitter = undefined export default class ScreenShotShareUtil { static startListener(callback){ const ScreenShotShare = NativeModules.ScreenShotShare screenCaptureEmitter && screenCaptureEmitter.removeAllListeners('ScreenShotShare') screenCaptureEmitter = Adapter.isIOS ? new NativeEventEmitter(ScreenShotShare) : DeviceEventEmitter screenCaptureEmitter.addListener('ScreenShotShare', (data) => { if(callback){ callback(data) } }) ScreenShotShare.startListener() return screenCaptureEmitter } static stopListener () { screenCaptureEmitter && screenCaptureEmitter.removeAllListeners('ScreenShotShare') const screenCaptureEmitter = NativeModules.ScreenShotShare return screenCaptureEmitter.stopListener() } static hasNavigationBar(){ if(!Adapter.isIOS){ screenCaptureEmitter && screenCaptureEmitter.removeAllListeners('ScreenShotShare') const screenCaptureEmitter = NativeModules.ScreenShotShare return screenCaptureEmitter.hasNavigationBar() }else{ return false } } } // ShareUtil.js // 分享图片 export const shareImage = (url, platform) => { platform = platform || 'weixin' let pl_int = 2 switch(platform){ case 'weixin': pl_int = 2 break case 'timeline': pl_int = 3 break case 'qq': pl_int = 0 break case 'qzone': pl_int = 4 break case 'weibo': pl_int = 1 break default: pl_int = 2 break } return new Promise((resolve, reject) => { UMShare.shareImage(url, IMAGE_URL, pl_int, (code, message) => { if(__DEV__){ console.log(`分享图片到${platform}`, code, message) } }) }) } 复制代码
调用
// index.js import ScreenShotShareModal from './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 ( <View style={{flex: 1, backgroundColor: '#fff', justifiContent: 'center', alignItems: 'center'}}> <Text>...</Text> <ScreenShotShareModal ref={ref => this.screenShotShareModal = ref} /> </View> ) } // ... // ScreenShotShareModal.js import { BaseDialog } from 'react-native-pickers' import { shareImage } from './ShareUtil' import QRCode from 'react-native-qrcode-svg' import ViewShot from 'react-native-view-shot' export default class ScreenShotShareModal extends BaseDialog { 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> <ViewShot ref={this.viewShot}> <View> <Image source={{uri: (isIOS ? this.state.image : `file://${this.state.image}`)}} /> <QRCode value={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> <View style={styles.itemGroup}> <View style={styles.item}> <TouchableOpacity activeOpacity={0.9} onPress={ () => this._shareImage('weixin') }> <Text>微信</Text> </TouchableOpacity> </View> <View style={styles.item}> <TouchableOpacity activeOpacity={0.9} onPress={() => this._shareImage('timeline') }> <Text>朋友圈</Text> </TouchableOpacity> </View> <View style={styles.item}> <TouchableOpacity activeOpacity={0.9} onPress={() => this._shareImage('weibo') }> <Text>微博</Text> </TouchableOpacity> </View> <View style={styles.item}> <TouchableOpacity activeOpacity={0.9} onPress={() => this._shareImage('qq') }> <Text>QQ</Text> </TouchableOpacity> </View> <View style={styles.item}> <TouchableOpacity activeOpacity={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) } }) } } const styles = StyleSheet.create({ itemGroup: { flexDirection: 'row', justifyContent: 'space-between', padding: 15 }, item: { justifyContent: 'center', alignItems: 'center', flexDirection: 'column' } }) 复制代码
以上所述就是小编给大家介绍的《React Native 实现截图添加二维码分享功能》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 取色器 ColorWanted 3.0-preview 发布,添加截图支持
- Flutter之屏幕截图/组件截图
- 学习笔记-HTML截图
- H5分享截图方案优化
- 如何优雅的完成长截图?
- Android之高效率截图
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
视觉SLAM十四讲
高翔、张涛、等 / 电子工业出版社 / 2017-3 / 75
《视觉SLAM十四讲:从理论到实践》系统介绍了视觉SLAM(同时定位与地图构建)所需的基本知识与核心算法,既包括数学理论基础,如三维空间的刚体运动、非线性优化,又包括计算机视觉的算法实现,例如多视图几何、回环检测等。此外,还提供了大量的实例代码供读者学习研究,从而更深入地掌握这些内容。 《视觉SLAM十四讲:从理论到实践》可以作为对SLAM 感兴趣的研究人员的入门自学材料,也可以作为SLAM......一起来看看 《视觉SLAM十四讲》 这本书的介绍吧!
MD5 加密
MD5 加密工具
UNIX 时间戳转换
UNIX 时间戳转换