React Native 實現截圖添加二維碼分享功能

截圖捕捉功能已經發布到 NPM,歡迎使用

npm i react-native-screenshotcatch
複製代碼

原文地址:http://liu-hang.cn/2019/06/11/185-react-native-screen-shot-share/javascript

對一個 JSer 來講,用原生來實現一個功能着實不容易。可是,隨着APP開發的深刻,在許多場景下RN現成的組件已經不能知足咱們的需求,不想受制於人就要本身動手。圖像繪製、文件系統、通知、模塊封裝等等,雖然難可是收穫也多,但願本身可以更深刻原生開發的領域。html

效果展現

效果展現GIF

截屏監聽功能

iOS 截屏監聽實現

實現思路:添加iOS自帶的UIApplicationUserDidTakeScreenshotNotification通知監聽,捕捉到事件後繪製當前頁面,保存返回文件地址java

// 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 方法本身繪製當前頁面而後保存,返回文件路徑。react

// 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,可是沒有現成的純圖片分享接口,須要本身封裝git

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'
  }
})
複製代碼

參考文章

iOS

Android

相關文章
相關標籤/搜索