gre / react-native-view-shot

Snapshot a React Native view and save it to an image
https://github.com/gre/react-native-view-shot-example
MIT License
2.69k stars 349 forks source link

[RNMapboxGL] Android Snapshots show highest level Map if using full screen overlays #294

Open FrederickEngelhardt opened 4 years ago

FrederickEngelhardt commented 4 years ago

bug report

Use case, I have two views with maps that need to 1:1 match each other. There is a parent view that uses overflow to cut off the top view so that the view below can show that map content.

When taking the view shot the overflow hidden property is not respected. Also it appears any additional layer with elevation above the MapboxGL MapView are ignored and not captured.

Version & Platform

├── react-native@0.61.5
└── react-native-view-shot@3.1.2

Platform: Android

Expected behavior

The screenshot should show the content that is viewable due to the overflow: 'hidden' property

Actual behavior

Overflow content is not viewable unless you set the width to a point value. (IE do not use left, right) instead use const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('screen')

Steps to reproduce the behavior

  1. Use this code
    
    import React, { useRef } from 'react'
    import { View, Text, TouchableOpacity, Dimensions } from 'react-native'
    import { captureRef } from 'react-native-view-shot'
    import Share from 'react-native-share'
    import Animated, { Extrapolate } from 'react-native-reanimated'
    import { PanGestureHandler } from 'react-native-gesture-handler'
    const { sub, event, set, Value } = Animated

const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('screen')

const Component = () => { const mapScreenshotRef = useRef(null) const handleShare = async () => { if (!mapScreenshotRef.current) return

const uri = await captureRef(mapScreenshotRef, {
  format: 'png',
  quality: 0.8,
})

if (!uri) {
  return
}

const title = 'Title'
const message = 'Please check this out!'
const options = {
  failOnCancel: false,
  url: uri,
  title,
  subject: title,
  message,
}
Share.open({ ...options })

}

const mapSizing = new Value(SCREEN_WIDTH * 1)

const onPanGestureEvent = event( [ { nativeEvent: { x: x => set(mapSizing, sub(mapSizing, x)), }, }, ], { useNativeDriver: true }, )

return ( <View style={{ flex: 1 }}> <View ref={mapScreenshotRef} collapsable={false} style={{ height: SCREEN_HEIGHT, width: SCREEN_WIDTH, }}

<View style={{ position: 'absolute', width: SCREEN_WIDTH, height: '100%', zIndex: 1, backgroundColor: 'blue', alignItems: 'center', justifyContent: 'center', }}

<Text style={{ color: 'yellow', fontWeight: 'bold', fontSize: 28 }}> I should overlay onto other text <View pointerEvents='box-none' style={{ shadowColor: '#000', shadowOffset: { width: -8, height: 0 }, shadowOpacity: 0.8, shadowRadius: 20, elevation: 3, zIndex: 1, height: '100%', }}

<Animated.View style={[ { elevation: 3, position: 'absolute', // width: SCREEN_WIDTH, // This will work right: 0, // this breaks on android for react-native-view-shot overflow: 'hidden', // hides the view that that bleeds out of this View zIndex: 0, height: SCREEN_HEIGHT, }, { left: mapSizing.interpolate({ inputRange: [0, SCREEN_WIDTH / 2, SCREEN_WIDTH], outputRange: [SCREEN_WIDTH - 50, SCREEN_WIDTH / 2, 50], extrapolate: Extrapolate.CLAMP, }), }, ]}

<View style={{ position: 'absolute', right: 0, width: SCREEN_WIDTH, height: SCREEN_HEIGHT, backgroundColor: 'red', }}

<Text style={{ color: 'yellow', fontWeight: 'bold', fontSize: 28 }}

I should overlay onto other text </Animated.View>

<TouchableOpacity style={{ zIndex: 50, elevation: 50, bottom: 50, padding: 24, position: 'absolute', backgroundColor: 'black', }} onPress={handleShare}

<Text style={{ color: 'white' }}>Click me to Share! ) }

export default Component

This is an example overlay that works. The moment you use <MapView> from mapbox GL and apply the same classes, things do not show up correctly on the screen.

Example code with mapboxGL integration

```javascript
import React, { useRef } from 'react'
import { View, Text, TouchableOpacity, Dimensions } from 'react-native'
import { captureRef } from 'react-native-view-shot'
import Share from 'react-native-share'
import Animated, { Extrapolate } from 'react-native-reanimated'
import { PanGestureHandler } from 'react-native-gesture-handler'
import MapboxGL, { RasterSourceProps } from '@react-native-mapbox-gl/maps'

const { sub, event, set, Value } = Animated

const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('screen')

const Component = () => {
  const mapScreenshotRef = useRef(null)
  const handleShare = async () => {
    if (!mapScreenshotRef.current) return

    const uri = await captureRef(mapScreenshotRef, {
      format: 'png',
      quality: 0.8,
    })

    if (!uri) {
      return
    }

    const title = 'Title'
    const message = 'Please check this out!'
    const options = {
      failOnCancel: false,
      url: uri,
      title,
      subject: title,
      message,
    }
    Share.open({ ...options })
  }

  const mapSizing = new Value(SCREEN_WIDTH * 1)

  const onPanGestureEvent = event(
    [
      {
        nativeEvent: {
          x: x => set(mapSizing, sub(mapSizing, x)),
        },
      },
    ],
    { useNativeDriver: true },
  )

  return (
    <View style={{ flex: 1 }}>
      <View
        ref={mapScreenshotRef}
        collapsable={false}
        style={{
          height: SCREEN_HEIGHT,
          width: SCREEN_WIDTH,
        }}
      >
        <View
          style={{
            position: 'absolute',
            width: SCREEN_WIDTH,
            height: '100%',
            zIndex: 1,
            backgroundColor: 'rgba(0,0,255,0.8)',
            alignItems: 'center',
            justifyContent: 'center',
          }}
        >
          <Text style={{ color: 'yellow', fontWeight: 'bold', fontSize: 28 }}>
            I should overlay onto other text
          </Text>
        </View>
        <View
          pointerEvents='box-none'
          style={{
            shadowColor: '#000',
            shadowOffset: { width: -8, height: 0 },
            shadowOpacity: 0.8,
            shadowRadius: 20,
            elevation: 3,
            zIndex: 1,
            height: '100%',
          }}
        >
          <Animated.View
            style={[
              {
                elevation: 3,
                position: 'absolute',
                // width: SCREEN_WIDTH, // This will work
                right: 0, // this breaks on android for react-native-view-shot
                overflow: 'hidden', // hides the view that that bleeds out of this View
                zIndex: 0,
                height: SCREEN_HEIGHT,
              },
              {
                left: mapSizing.interpolate({
                  inputRange: [0, SCREEN_WIDTH / 2, SCREEN_WIDTH],
                  outputRange: [SCREEN_WIDTH - 50, SCREEN_WIDTH / 2, 50],
                  extrapolate: Extrapolate.CLAMP,
                }),
              },
            ]}
          >
            <View
              style={{
                position: 'absolute',
                right: 0,
                width: SCREEN_WIDTH,
                height: SCREEN_HEIGHT,
                backgroundColor: 'rgba(255,0,0,0.8)',
              }}
            >
              <Text
                style={{ color: 'yellow', fontWeight: 'bold', fontSize: 28 }}
              >
                I should overlay onto other text
              </Text>
              <MapboxGL.MapView
                styleURL={MapboxGL.StyleURL.Dark}
              ></MapboxGL.MapView>
            </View>
          </Animated.View>
        </View>
      </View>
      <PanGestureHandler onGestureEvent={onPanGestureEvent}>
        <Animated.View
          style={[
            {
              position: 'absolute',
              zIndex: 9999,
              elevation: 9999,
              height: '100%',
              width: 30,
              backgroundColor: 'green',
            },
            {
              transform: [
                {
                  translateX: mapSizing.interpolate({
                    inputRange: [0, SCREEN_WIDTH / 2, SCREEN_WIDTH],
                    outputRange: [SCREEN_WIDTH - 65, SCREEN_WIDTH / 2 - 15, 35],
                    extrapolate: Extrapolate.CLAMP,
                  }),
                },
              ],
            },
          ]}
        ></Animated.View>
      </PanGestureHandler>
      <TouchableOpacity
        style={{
          zIndex: 50,
          elevation: 50,
          bottom: 50,
          padding: 24,
          position: 'absolute',
          backgroundColor: 'black',
        }}
        onPress={handleShare}
      >
        <Text style={{ color: 'white' }}>Click me to Share!</Text>
      </TouchableOpacity>
    </View>
  )
}

export default Component
FrederickEngelhardt commented 4 years ago

Here's expected https://drive.google.com/open?id=1moOxEmY1VpNaFGHCxJ3c8rDqaItuBzW3 vs actual https://drive.google.com/open?id=1dQovj64sILBEGamZkx-39sm2CHQIi-t-

chrfalch commented 1 year ago

We've made some fixes to the code in a Pull Request that might be interesting for this repo as well:

https://github.com/Shopify/react-native-skia/pull/1642