software-mansion / react-native-svg

SVG library for React Native, React Native Web, and plain React web projects.
MIT License
7.44k stars 1.12k forks source link

Ugly Android performance with PanResponder #1064

Closed tumanov-alex closed 5 years ago

tumanov-alex commented 5 years ago

Try to zoom — urgh!

import React from 'react'
import {
  Dimensions,
  PanResponder,
  Animated,
} from 'react-native'
import { Svg, Rect, G } from 'react-native-svg'

const { width, height } = Dimensions.get('window')
const GAnimated = Animated.createAnimatedComponent(G)

// utils beginning
function calcDistance(x1, y1, x2, y2) {
  const dx = x1 - x2
  const dy = y1 - y2
  return Math.sqrt(dx * dx + dy * dy)
}
const range = (min, max) =>
  Math.floor(Math.random() * (
    max - min + 1
  )) + min
const genPoly = (count) => {
  return Array.from({ length: count }).map((_, i) => {
    const x = range(20, 320)
    const y = range(170, 470)
    const radius = range(10, 30)

    return <Rect
      onPress={() => alert(i)}
      x={x}
      y={y}
      width={radius}
      height={radius}
      fill={`rgba(${range(0, 255)}, ${range(0, 255)}, ${range(0, 255)}, 0.2)`}
      strokeWidth={1}
      stroke='black'
      key={i}
      style={{
        zIndex: 100,
      }}
    />
  })
}
const polygons = genPoly(100)
// utils ending

function ZoomProblem() {
  const zoom = new Animated.Value(1)
  const position = new Animated.ValueXY()
  let pinchDistanceInit = 0
  let zoomInit = zoom.__getValue()
  const panResponder = PanResponder.create({
    onStartShouldSetPanResponder: (evt, gestureState) => false,
    onStartShouldSetPanResponderCapture: (evt, gestureState) => false,
    onMoveShouldSetPanResponder: (evt, gestureState) => true,
    onMoveShouldSetPanResponderCapture: (evt, gestureState) => false,
    onPanResponderTerminationRequest: (evt, gestureState) => true,
    onPanResponderTerminate: (evt, gestureState) => {},
    onPanResponderGrant: (evt) => {
      if (evt.nativeEvent.touches.length === 2) {
        const [
          { locationX: x1, locationY: y1 },
          { locationX: x2, locationY: y2 },
        ] = evt.nativeEvent.touches

        pinchDistanceInit = calcDistance(x1, y1, x2, y2)
        zoomInit = zoom.__getValue()
      }

      position.setOffset(position.__getValue()) // prevents offset from resetting
    },
    onPanResponderMove: (evt, gesture) => {
      switch (gesture.numberActiveTouches) {
        case 1: {
          position.setValue({ x: gesture.dx, y: gesture.dy })
          break
        }
        case 2: {
          const [
            { locationX: x1, locationY: y1 },
            { locationX: x2, locationY: y2 },
          ] = evt.nativeEvent.touches
          const distance = calcDistance(x1, y1, x2, y2)

          Animated.timing(
            zoom,
            {
              toValue: zoomInit * (distance / pinchDistanceInit),
              duration: 0,
              useNativeDriver: true,
            },
          ).start()
          break
        }
      }
    },
  })
  const { top, left } = position.getLayout()

  return (
    <Animated.View
      {...panResponder.panHandlers}
      style={{
        transform: [
          { translateX: left },
          { translateY: top },
        ],
      }}
    >
      <Svg
        width={width}
        height={height}
      >
        <GAnimated
          style={{
            transform: [{ scale: zoom }],
          }}
        >
          {polygons}
        </GAnimated>
      </Svg>
    </Animated.View>
  )
}
msand commented 5 years ago

Feel free to do profiling and identify optimization opportunities

msand commented 5 years ago

You might want to try https://www.npmjs.com/package/zoomable-svg https://github.com/msand/InfiniDraw/ https://infinidraw.live/ It's a bit full of stuff in the origin, so even chrome on desktop has trouble zooming until you get out far enough.

msand commented 5 years ago

Seems quite smooth with react-native-gesture-handler:

import React, { Component, PureComponent } from 'react';
import { Animated, StyleSheet, Text, View, Dimensions } from 'react-native';

import {
  PanGestureHandler,
  PinchGestureHandler,
  RotationGestureHandler,
  State,
} from 'react-native-gesture-handler';

import { Svg, Rect, G } from 'react-native-svg';

const { width, height } = Dimensions.get('window');
const GAnimated = Animated.createAnimatedComponent(G);

const range = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
const genPoly = count => {
  return Array.from({ length: count }).map((_, i) => {
    const x = range(20, 320);
    const y = range(170, 470);
    const radius = range(10, 30);

    return (
      <Rect
        onPress={() => alert(i)}
        x={x}
        y={y}
        width={radius}
        height={radius}
        fill={`rgba(${range(0, 255)}, ${range(0, 255)}, ${range(0, 255)}, 0.2)`}
        strokeWidth={1}
        stroke="black"
        key={i}
        style={{
          zIndex: 100,
        }}
      />
    );
  });
};
const polygons = genPoly(100);

const USE_NATIVE_DRIVER = true;

export class PinchableBox extends React.Component {
  panRef = React.createRef();
  rotationRef = React.createRef();
  pinchRef = React.createRef();
  constructor(props) {
    super(props);

    /* Pinching */
    this._baseScale = new Animated.Value(1);
    this._pinchScale = new Animated.Value(1);
    this._scale = Animated.multiply(this._baseScale, this._pinchScale);
    this._lastScale = 1;
    this._onPinchGestureEvent = Animated.event(
      [{ nativeEvent: { scale: this._pinchScale } }],
      { useNativeDriver: USE_NATIVE_DRIVER },
    );

    /* Rotation */
    this._rotate = new Animated.Value(0);
    this._rotateStr = this._rotate.interpolate({
      inputRange: [-100, 100],
      outputRange: ['-100rad', '100rad'],
    });
    this._lastRotate = 0;
    this._onRotateGestureEvent = Animated.event(
      [{ nativeEvent: { rotation: this._rotate } }],
      { useNativeDriver: USE_NATIVE_DRIVER },
    );

    /* Tilt */
    this._tilt = new Animated.Value(0);
    this._tiltStr = this._tilt.interpolate({
      inputRange: [-501, -500, 0, 1],
      outputRange: ['1rad', '1rad', '0rad', '0rad'],
    });
    this._lastTilt = 0;
    this._onTiltGestureEvent = Animated.event(
      [{ nativeEvent: { translationY: this._tilt } }],
      { useNativeDriver: USE_NATIVE_DRIVER },
    );
  }

  _onRotateHandlerStateChange = event => {
    if (event.nativeEvent.oldState === State.ACTIVE) {
      this._lastRotate += event.nativeEvent.rotation;
      this._rotate.setOffset(this._lastRotate);
      this._rotate.setValue(0);
    }
  };
  _onPinchHandlerStateChange = event => {
    if (event.nativeEvent.oldState === State.ACTIVE) {
      this._lastScale *= event.nativeEvent.scale;
      this._baseScale.setValue(this._lastScale);
      this._pinchScale.setValue(1);
    }
  };
  _onTiltGestureStateChange = event => {
    if (event.nativeEvent.oldState === State.ACTIVE) {
      this._lastTilt += event.nativeEvent.translationY;
      this._tilt.setOffset(this._lastTilt);
      this._tilt.setValue(0);
    }
  };
  render() {
    return (
      <PanGestureHandler
        ref={this.panRef}
        onGestureEvent={this._onTiltGestureEvent}
        onHandlerStateChange={this._onTiltGestureStateChange}
        minDist={10}
        minPointers={2}
        maxPointers={2}
        avgTouches
      >
        <Animated.View style={styles.wrapper}>
          <RotationGestureHandler
            ref={this.rotationRef}
            simultaneousHandlers={this.pinchRef}
            onGestureEvent={this._onRotateGestureEvent}
            onHandlerStateChange={this._onRotateHandlerStateChange}
          >
            <Animated.View style={styles.wrapper}>
              <PinchGestureHandler
                ref={this.pinchRef}
                simultaneousHandlers={this.rotationRef}
                onGestureEvent={this._onPinchGestureEvent}
                onHandlerStateChange={this._onPinchHandlerStateChange}
              >
                <Animated.View style={styles.container} collapsable={false}>
                  <Svg
                    width={width}
                    height={height}
                    viewBox="40 130 330 500"
                    style={styles.pinchableImage}
                  >
                    <GAnimated
                      style={{
                        transform: [
                          { perspective: 200 },
                          { translateX: width / 2 },
                          { translateY: height / 2 },
                          { scale: this._scale },
                          { rotate: this._rotateStr },
                          { translateX: -width / 2 },
                          { translateY: -height / 2 },
                        ],
                      }}
                    >
                      {polygons}
                    </GAnimated>
                  </Svg>
                </Animated.View>
              </PinchGestureHandler>
            </Animated.View>
          </RotationGestureHandler>
        </Animated.View>
      </PanGestureHandler>
    );
  }
}

export default PinchableBox;

const styles = StyleSheet.create({
  container: {
    ...StyleSheet.absoluteFillObject,
    backgroundColor: 'black',
    overflow: 'hidden',
    alignItems: 'center',
    flex: 1,
    justifyContent: 'center',
  },
  pinchableImage: {
    backgroundColor: 'white',
    width,
    height,
  },
  wrapper: {
    flex: 1,
  },
});
msand commented 5 years ago

Another example with proper translation instead:

import React, { Component, PureComponent } from 'react';
import { Animated, StyleSheet, Text, View, Dimensions } from 'react-native';

import {
  PanGestureHandler,
  PinchGestureHandler,
  RotationGestureHandler,
  State,
} from 'react-native-gesture-handler';

import { Svg, Rect, G } from 'react-native-svg';

const { width, height } = Dimensions.get('window');
const GAnimated = Animated.createAnimatedComponent(G);

const range = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
const genPoly = count => {
  return Array.from({ length: count }).map((_, i) => {
    const x = range(20, 320);
    const y = range(170, 470);
    const radius = range(10, 30);

    return (
      <Rect
        onPress={() => alert(i)}
        x={x}
        y={y}
        width={radius}
        height={radius}
        fill={`rgba(${range(0, 255)}, ${range(0, 255)}, ${range(0, 255)}, 0.2)`}
        strokeWidth={1}
        stroke="black"
        key={i}
        style={{
          zIndex: 100,
        }}
      />
    );
  });
};
const polygons = genPoly(100);
class Polygons extends PureComponent {
  render() {
    return polygons;
  }
}

const USE_NATIVE_DRIVER = true;

export class PinchableBox extends React.Component {
  panRef = React.createRef();
  rotationRef = React.createRef();
  pinchRef = React.createRef();
  constructor(props) {
    super(props);

    /* Pinching */
    this._baseScale = new Animated.Value(1);
    this._pinchScale = new Animated.Value(1);
    this._scale = Animated.multiply(this._baseScale, this._pinchScale);
    this._lastScale = 1;
    this._onPinchGestureEvent = Animated.event(
      [{ nativeEvent: { scale: this._pinchScale } }],
      { useNativeDriver: USE_NATIVE_DRIVER },
    );

    /* Rotation */
    this._rotate = new Animated.Value(0);
    this._rotateStr = this._rotate.interpolate({
      inputRange: [-100, 100],
      outputRange: ['-100rad', '100rad'],
    });
    this._lastRotate = 0;
    this._onRotateGestureEvent = Animated.event(
      [{ nativeEvent: { rotation: this._rotate } }],
      { useNativeDriver: USE_NATIVE_DRIVER },
    );

    /* Translate */
    this._translateX = new Animated.Value(0);
    this._translateY = new Animated.Value(0);
    this._lastOffset = { x: 0, y: 0 };
    this._onGestureEvent = Animated.event(
      [
        {
          nativeEvent: {
            translationX: this._translateX,
            translationY: this._translateY,
          },
        },
      ],
      { useNativeDriver: USE_NATIVE_DRIVER },
    );
  }

  _onRotateHandlerStateChange = event => {
    if (event.nativeEvent.oldState === State.ACTIVE) {
      this._lastRotate += event.nativeEvent.rotation;
      this._rotate.setOffset(this._lastRotate);
      this._rotate.setValue(0);
    }
  };
  _onPinchHandlerStateChange = event => {
    if (event.nativeEvent.oldState === State.ACTIVE) {
      this._lastScale *= event.nativeEvent.scale;
      this._baseScale.setValue(this._lastScale);
      this._pinchScale.setValue(1);
    }
  };
  _onHandlerStateChange = event => {
    if (event.nativeEvent.oldState === State.ACTIVE) {
      this._lastOffset.x += event.nativeEvent.translationX;
      this._lastOffset.y += event.nativeEvent.translationY;
      this._translateX.setOffset(this._lastOffset.x);
      this._translateX.setValue(0);
      this._translateY.setOffset(this._lastOffset.y);
      this._translateY.setValue(0);
    }
  };
  render() {
    return (
      <PanGestureHandler
        ref={this.panRef}
        onGestureEvent={this._onGestureEvent}
        onHandlerStateChange={this._onHandlerStateChange}
      >
        <Animated.View style={styles.wrapper}>
          <RotationGestureHandler
            ref={this.rotationRef}
            simultaneousHandlers={this.pinchRef}
            onGestureEvent={this._onRotateGestureEvent}
            onHandlerStateChange={this._onRotateHandlerStateChange}
          >
            <Animated.View style={styles.wrapper}>
              <PinchGestureHandler
                ref={this.pinchRef}
                simultaneousHandlers={this.rotationRef}
                onGestureEvent={this._onPinchGestureEvent}
                onHandlerStateChange={this._onPinchHandlerStateChange}
              >
                <Animated.View style={styles.container} collapsable={false}>
                  <Svg
                    width={width}
                    height={height}
                    viewBox="40 130 330 500"
                    style={styles.pinchableImage}
                  >
                    <GAnimated
                      style={{
                        transform: [
                          { translateX: this._translateX },
                          { translateY: this._translateY },
                          { perspective: 200 },
                          { translateX: width / 2 },
                          { translateY: height / 2 },
                          { scale: this._scale },
                          { rotate: this._rotateStr },
                          { translateX: -width / 2 },
                          { translateY: -height / 2 },
                        ],
                      }}
                    >
                      <Polygons />
                    </GAnimated>
                  </Svg>
                </Animated.View>
              </PinchGestureHandler>
            </Animated.View>
          </RotationGestureHandler>
        </Animated.View>
      </PanGestureHandler>
    );
  }
}

export default PinchableBox;

const styles = StyleSheet.create({
  container: {
    ...StyleSheet.absoluteFillObject,
    backgroundColor: 'black',
    overflow: 'hidden',
    alignItems: 'center',
    flex: 1,
    justifyContent: 'center',
  },
  pinchableImage: {
    backgroundColor: 'white',
    width,
    height,
  },
  wrapper: {
    flex: 1,
  },
});
msand commented 5 years ago

@tumanov-alex Can you try with the latest commit from the develop branch? https://github.com/react-native-community/react-native-svg/commit/defce98ac90f174f68dac817aa701d66f9535bc1 I've optimized the rendering performance on Android quite a bit, found two low hanging fruits by profiling my latest example.

msand commented 5 years ago

Simplified version for android:

import React, { Component } from 'react';
import { Svg, Rect, G } from 'react-native-svg';
import { Animated, StyleSheet, Dimensions } from 'react-native';
import {
  PanGestureHandler,
  PinchGestureHandler,
  State,
} from 'react-native-gesture-handler';

const { width, height } = Dimensions.get('window');
const AnimatedG = Animated.createAnimatedComponent(G);
const AnimatedSvg = Animated.createAnimatedComponent(Svg);

const range = (min, max) => Math.random() * (max - min) + min;
const ri = (min, max) => Math.floor(Math.random() * (max - min) + 1) + min;
const genPoly = count => {
  return Array.from({ length: count }).map((_, i) => {
    const x = range(0.05, 0.95) * width;
    const y = range(0.05, 0.95) * width;
    const side = range(0.01, 0.05) * width;
    return (
      <Rect
        x={x}
        y={y}
        key={i}
        width={side}
        height={side}
        stroke="black"
        onPress={() => alert(i)}
        fill={`rgba(${ri(0, 255)}, ${ri(0, 255)}, ${ri(0, 255)}, 0.2)`}
      />
    );
  });
};
const polygons = genPoly(100);
class Polygons extends Component {
  shouldComponentUpdate() {
    return false;
  }
  render() {
    return polygons;
  }
}

const USE_NATIVE_DRIVER = true;

export class PinchableBox extends Component {
  /* Pan */
  panRef = React.createRef();
  lastOffset = { x: 0, y: 0 };
  translateX = new Animated.Value(0);
  translateY = new Animated.Value(0);
  onPanGestureEvent = Animated.event(
    [
      {
        nativeEvent: {
          translationX: this.translateX,
          translationY: this.translateY,
        },
      },
    ],
    { useNativeDriver: USE_NATIVE_DRIVER },
  );
  onPanHandlerStateChange = ({ nativeEvent }) => {
    if (nativeEvent.oldState === State.ACTIVE) {
      const { lastOffset, translateX, translateY } = this;
      const { translationX, translationY } = nativeEvent;
      lastOffset.x += translationX;
      lastOffset.y += translationY;
      const { x, y } = lastOffset;
      translateX.setOffset(x);
      translateY.setOffset(y);
      translateX.setValue(0);
      translateY.setValue(0);
    }
  };

  /* Pinch */
  lastScale = 1;
  pinchRef = React.createRef();
  baseScale = new Animated.Value(1);
  pinchScale = new Animated.Value(1);
  scale = Animated.multiply(this.baseScale, this.pinchScale);
  onPinchGestureEvent = Animated.event(
    [{ nativeEvent: { scale: this.pinchScale } }],
    { useNativeDriver: USE_NATIVE_DRIVER },
  );
  onPinchHandlerStateChange = ({ nativeEvent }) => {
    if (nativeEvent.oldState === State.ACTIVE) {
      const { baseScale, pinchScale } = this;
      this.lastScale *= nativeEvent.scale;
      baseScale.setValue(this.lastScale);
      pinchScale.setValue(1);
    }
  };

  render() {
    let y = width / 2;
    return (
      <PanGestureHandler
        ref={this.panRef}
        onGestureEvent={this.onPanGestureEvent}
        onHandlerStateChange={this.onPanHandlerStateChange}
      >
        <Animated.View style={styles.wrapper}>
          <PinchGestureHandler
            ref={this.pinchRef}
            simultaneousHandlers={this.panRef}
            onGestureEvent={this.onPinchGestureEvent}
            onHandlerStateChange={this.onPinchHandlerStateChange}
          >
            <AnimatedSvg
              width={width}
              height={height}
              style={styles.container}
              viewBox={`0 0 ${width} ${width}`}
            >
              <AnimatedG
                style={{
                  transform: [
                    { translateX: this.translateX },
                    { translateY: this.translateY },
                    { translateY: y },
                    { scale: this.scale },
                    { translateY: -y },
                  ],
                }}
              >
                <Polygons />
              </AnimatedG>
            </AnimatedSvg>
          </PinchGestureHandler>
        </Animated.View>
      </PanGestureHandler>
    );
  }
}

export default PinchableBox;

const styles = StyleSheet.create({
  container: {
    ...StyleSheet.absoluteFillObject,
    backgroundColor: 'white',
    width,
    height,
  },
  wrapper: {
    width,
    height,
  },
});
msand commented 5 years ago

Another example using reanimated:

import React, { Component } from 'react';
import { StyleSheet, Dimensions, View, Platform } from 'react-native';
import { Svg, Rect, G } from 'react-native-svg';
import Animated from 'react-native-reanimated';
import {
  State,
  PanGestureHandler,
  PinchGestureHandler,
  RotationGestureHandler,
} from 'react-native-gesture-handler';

const android = Platform.OS === 'android';
const { width, height } = Dimensions.get('window');
const AnimatedG = Animated.createAnimatedComponent(G);
const { set, cond, block, eq, add, Value, event, concat, multiply } = Animated;

const halfWidth = width / 2;
const [originX, originY] = android ? [0, width] : [halfWidth, halfWidth];

const max = 0.9 * width;
const side = 0.1 * width;
const pos = () => Math.random() * max;
const rbyte = () => Math.floor(Math.random() * 256);
const fill = () => `rgba(${rbyte()}, ${rbyte()}, ${rbyte()}, 0.2)`;

const polygons = Array.from({ length: 200 }).map((_, i) => (
  <Rect
    key={i}
    x={pos()}
    y={pos()}
    width={side}
    height={side}
    fill={fill()}
    stroke="black"
    onPress={alert.bind(alert, i)}
  />
));

class Polygons extends Component {
  shouldComponentUpdate() {
    return false;
  }
  render() {
    return polygons;
  }
}

export default class Example extends Component {
  constructor(props) {
    super(props);
    this.X = new Value(0);
    this.Y = new Value(0);
    this.R = new Value(0);
    this.Z = new Value(1);
    const offsetX = new Value(0);
    const offsetY = new Value(0);
    const offsetR = new Value(0);
    const offsetZ = new Value(1);

    this.handlePan = event([
      {
        nativeEvent: ({ translationX: x, translationY: y, state }) => {
          console.log(state);
          return block([
            set(this.X, add(x, offsetX)),
            set(this.Y, add(y, offsetY)),
            cond(eq(state, State.END), [
              set(offsetX, add(offsetX, x)),
              set(offsetY, add(offsetY, y)),
            ]),
          ]);
        },
      },
    ]);

    this.handlePinch = event([
      {
        nativeEvent: ({ scale: z, state }) => {
          console.log(state);
          return block([
            cond(eq(state, State.ACTIVE), set(this.Z, multiply(z, offsetZ))),
            cond(eq(state, State.END), [set(offsetZ, multiply(offsetZ, z))]),
          ]);
        },
      },
    ]);

    this.handleRotate = event([
      {
        nativeEvent: ({ rotation: r, state }) => {
          console.log(state);
          return block([
            set(this.R, add(r, offsetR)),
            cond(eq(state, State.END), [set(offsetR, add(offsetR, r))]),
          ]);
        },
      },
    ]);
  }

  panRef = React.createRef();
  pinchRef = React.createRef();
  rotationRef = React.createRef();

  render() {
    return (
      <View style={styles.container}>
        <PanGestureHandler
          averageTouches
          ref={this.panRef}
          onGestureEvent={this.handlePan}
          onHandlerStateChange={this.handlePan}
          simultaneousHandlers={[this.rotationRef, this.pinchRef]}
        >
          <Animated.View>
            <PinchGestureHandler
              ref={this.pinchRef}
              onGestureEvent={this.handlePinch}
              onHandlerStateChange={this.handlePinch}
              simultaneousHandlers={[this.rotationRef, this.panRef]}
            >
              <Animated.View>
                <RotationGestureHandler
                  ref={this.rotationRef}
                  onGestureEvent={this.handleRotate}
                  onHandlerStateChange={this.handleRotate}
                  simultaneousHandlers={[this.pinchRef, this.panRef]}
                >
                  <Animated.View>
                    <Svg
                      width={width}
                      height={height}
                      viewBox={`0 0 ${width} ${width}`}
                    >
                      <AnimatedG
                        style={{
                          transform: [
                            { translateX: this.X },
                            { translateY: this.Y },
                            { translateX: originX },
                            { translateY: originY },
                            { rotate: concat(this.R, 'rad') },
                            { scale: this.Z },
                            { translateX: -originX },
                            { translateY: -originY },
                          ],
                        }}
                      >
                        <Rect
                          x={0}
                          y={0}
                          width={width}
                          height={width}
                          fill={fill()}
                        />
                        <Polygons />
                      </AnimatedG>
                    </Svg>
                  </Animated.View>
                </RotationGestureHandler>
              </Animated.View>
            </PinchGestureHandler>
          </Animated.View>
        </PanGestureHandler>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    ...StyleSheet.absoluteFillObject,
    backgroundColor: 'white',
    width,
    height,
  },
});
msand commented 5 years ago

Published v9.6.1 with the improvements.

msand commented 5 years ago

If you remove the onPress handlers, it's even faster, as it doesn't need to make certain calls to enable correct tracking of touches + movements.

msand commented 5 years ago

Hooks instead of components:

import React, { useRef, memo } from 'react';
import { StyleSheet, Dimensions, View, Platform } from 'react-native';
import { Svg, Rect, G } from 'react-native-svg';
import Animated from 'react-native-reanimated';
import {
  State,
  PanGestureHandler,
  PinchGestureHandler,
  RotationGestureHandler,
} from 'react-native-gesture-handler';

const android = Platform.OS === 'android';
const { width, height } = Dimensions.get('window');
const AnimatedG = Animated.createAnimatedComponent(G);
const { set, cond, block, eq, add, Value, event, concat, multiply } = Animated;

const max = 0.9 * width;
const side = 0.1 * width;
const origin = -width / 2;
const ratio = width / height;
const oX = android ? origin : 0;

const pos = () => Math.random() * max;
const byte = () => Math.floor(Math.random() * 256);
const fill = () => `rgba(${byte()}, ${byte()}, ${byte()}, 0.2)`;

const polygons = Array.from({ length: 200 }).map((_, i) => (
  <Rect
    key={i}
    x={pos()}
    y={pos()}
    width={side}
    height={side}
    fill={fill()}
    stroke="black"
  />
));

const Polygons = memo(() => polygons, () => true);

export default () => {
  const X = new Value(0);
  const Y = new Value(0);
  const R = new Value(0);
  const Z = new Value(1);
  const offsetX = new Value(0);
  const offsetY = new Value(0);
  const offsetR = new Value(0);
  const offsetZ = new Value(1);

  const handlePan = event([
    {
      nativeEvent: ({ translationX: x, translationY: y, state }) =>
        block([
          set(X, add(x, offsetX)),
          set(Y, add(y, offsetY)),
          cond(eq(state, State.END), [
            set(offsetX, add(offsetX, x)),
            set(offsetY, add(offsetY, y)),
          ]),
        ]),
    },
  ]);
  const handlePinch = event([
    {
      nativeEvent: ({ scale: z, state }) =>
        block([
          cond(eq(state, State.ACTIVE), set(Z, multiply(z, offsetZ))),
          cond(eq(state, State.END), [set(offsetZ, multiply(offsetZ, z))]),
        ]),
    },
  ]);
  const handleRotation = event([
    {
      nativeEvent: ({ rotation: r, state }) =>
        block([
          set(R, add(r, offsetR)),
          cond(eq(state, State.END), [set(offsetR, add(offsetR, r))]),
        ]),
    },
  ]);

  const panRef = useRef(null);
  const pinchRef = useRef(null);
  const rotationRef = useRef(null);

  return (
    <View style={styles.container}>
      <PanGestureHandler
        ref={panRef}
        avgTouches
        onGestureEvent={handlePan}
        onHandlerStateChange={handlePan}
        simultaneousHandlers={[rotationRef, pinchRef]}
      >
        <Animated.View>
          <PinchGestureHandler
            ref={pinchRef}
            onGestureEvent={handlePinch}
            onHandlerStateChange={handlePinch}
            simultaneousHandlers={[rotationRef, panRef]}
          >
            <Animated.View>
              <RotationGestureHandler
                ref={rotationRef}
                onGestureEvent={handleRotation}
                onHandlerStateChange={handleRotation}
                simultaneousHandlers={[pinchRef, panRef]}
              >
                <Animated.View>
                  <Svg
                    width={width}
                    height={height}
                    viewBox={`${origin} ${origin * ratio} ${width} ${width}`}
                  >
                    <AnimatedG
                      style={{
                        transform: [
                          { translateX: X },
                          { translateY: Y },
                          { translateX: oX },
                          { rotate: concat(R, 'rad') },
                          { scale: Z },
                          { translateX: -oX },
                        ],
                      }}
                    >
                      <G transform={`translate(${origin},${origin})`}>
                        <Rect
                          x={0}
                          y={0}
                          width={width}
                          height={width}
                          fill={fill()}
                        />
                        <Polygons />
                      </G>
                    </AnimatedG>
                  </Svg>
                </Animated.View>
              </RotationGestureHandler>
            </Animated.View>
          </PinchGestureHandler>
        </Animated.View>
      </PanGestureHandler>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    ...StyleSheet.absoluteFillObject,
    backgroundColor: 'white',
  },
});
msand commented 5 years ago

Slightly more advanced viewer example, adapted to svg and slightly stripped down version from: https://github.com/kmagiera/react-native-gesture-handler/issues/546#issuecomment-481341466 https://github.com/Ashoat/squadcal/blob/master/native/media/multimedia-modal.react.js

import React, { memo } from 'react';
import { StyleSheet, Dimensions, View, Platform, Text, TouchableOpacity } from 'react-native';
import { Svg, Rect, G } from 'react-native-svg';
import Animated, { Easing } from 'react-native-reanimated';
import {
  State,
  PanGestureHandler,
  PinchGestureHandler,
  TapGestureHandler,
} from 'react-native-gesture-handler';

const android = Platform.OS === 'android';
const { width, height } = Dimensions.get('window');
const AnimatedG = Animated.createAnimatedComponent(G);

const {
  Value,
  Clock,
  event,
  Extrapolate,
  set,
  call,
  cond,
  not,
  and,
  or,
  eq,
  neq,
  greaterThan,
  add,
  sub,
  multiply,
  divide,
  pow,
  max,
  min,
  round,
  abs,
  interpolate,
  startClock,
  stopClock,
  clockRunning,
  timing,
  decay,
  diffClamp
} = Animated;

function clamp(value: Value, minValue: Value, maxValue: Value): Value {
  return cond(
    greaterThan(value, maxValue),
    maxValue,
    cond(
      greaterThan(minValue, value),
      minValue,
      value,
    ),
  );
}

function scaleDelta(value: Value, gestureActive: Value) {
  const diffThisFrame = new Value(1);
  const prevValue = new Value(1);
  return cond(
    gestureActive,
    [
      set(diffThisFrame, divide(value, prevValue)),
      set(prevValue, value),
      diffThisFrame,
    ],
    set(prevValue, 1),
  );
}

function panDelta(value: Value, gestureActive: Value) {
  const diffThisFrame = new Value(0);
  const prevValue = new Value(0);
  return cond(
    gestureActive,
    [
      set(diffThisFrame, sub(value, prevValue)),
      set(prevValue, value),
      diffThisFrame,
    ],
    set(prevValue, 0),
  );
}

function gestureJustEnded(tapState: Value) {
  const prevValue = new Value(-1);
  return cond(
    eq(prevValue, tapState),
    0,
    [
      set(prevValue, tapState),
      eq(tapState, State.END),
    ],
  );
}

function runTiming(
  clock: Clock,
  initialValue: Value | number,
  finalValue: Value | number,
  startStopClock: bool = true,
): Value {
  const state = {
    finished: new Value(0),
    position: new Value(0),
    frameTime: new Value(0),
    time: new Value(0),
  };
  const config = {
    toValue: new Value(0),
    duration: 250,
    easing: Easing.out(Easing.ease),
  };
  return [
    cond(
      not(clockRunning(clock)),
      [
        set(state.finished, 0),
        set(state.frameTime, 0),
        set(state.time, 0),
        set(state.position, initialValue),
        set(config.toValue, finalValue),
        startStopClock && startClock(clock),
      ],
    ),
    timing(clock, state, config),
    cond(
      state.finished,
      startStopClock && stopClock(clock),
    ),
    state.position,
  ];
}

function runDecay(
  clock: Clock,
  velocity: Value,
  initialPosition: Value,
  startStopClock: bool = true,
): Value {
  const state = {
    finished: new Value(0),
    velocity: new Value(0),
    position: new Value(0),
    time: new Value(0),
  };
  const config = { deceleration: 0.99 };
  return [
    cond(
      not(clockRunning(clock)),
      [
        set(state.finished, 0),
        set(state.velocity, velocity),
        set(state.position, initialPosition),
        set(state.time, 0),
        startStopClock && startClock(clock),
      ],
    ),
    decay(clock, state, config),
    set(velocity, state.velocity),
    cond(
      state.finished,
      startStopClock && stopClock(clock),
    ),
    state.position,
  ];
}

class MultimediaModal extends React.PureComponent {

  centerX = new Value(0);
  centerY = new Value(0);
  screenWidth = new Value(0);
  screenHeight = new Value(0);
  imageWidth = new Value(0);
  imageHeight = new Value(0);

  pinchHandler = React.createRef();
  panHandler = React.createRef();
  tapHandler = React.createRef();
  handlerRefs = [ this.pinchHandler, this.panHandler, this.tapHandler ];
  priorityHandlerRefs = [ this.pinchHandler, this.panHandler ];

  pinchEvent;
  panEvent;
  tapEvent;

  scale: Value;
  x: Value;
  y: Value;
  opacity: Value;
  imageContainerOpacity: Value;

  constructor(props: Props) {
    super(props);
    this.updateDimensions();

    const { screenWidth, screenHeight, imageWidth, imageHeight } = this;
    const left = sub(this.centerX, divide(imageWidth, 2));
    const top = sub(this.centerY, divide(imageHeight, 2));

    const initialCoordinates = { x: 0, y: 0, width, height };
    const initialScale = divide(
      initialCoordinates.width,
      imageWidth,
    );
    const initialTranslateX = sub(
      initialCoordinates.x + initialCoordinates.width / 2,
      add(left, divide(imageWidth, 2)),
    );
    const initialTranslateY = sub(
      initialCoordinates.y + initialCoordinates.height / 2,
      add(top, divide(imageHeight, 2)),
    );

    const position = new Value(1);
    const progress = interpolate(
      position,
      {
        inputRange: [ 0, 1 ],
        outputRange: [ 0, 1 ],
        extrapolate: Extrapolate.CLAMP,
      },
    );

    // The inputs we receive from PanGestureHandler
    const panState = new Value(-1);
    const panTranslationX = new Value(0);
    const panTranslationY = new Value(0);
    const panVelocityX = new Value(0);
    const panVelocityY = new Value(0);
    this.panEvent = event([{
      nativeEvent: {
        state: panState,
        translationX: panTranslationX,
        translationY: panTranslationY,
        velocityX: panVelocityX,
        velocityY: panVelocityY,
      },
    }]);
    const panActive = eq(panState, State.ACTIVE);

    // The inputs we receive from PinchGestureHandler
    const pinchState = new Value(-1);
    const pinchScale = new Value(1);
    const pinchFocalX = new Value(0);
    const pinchFocalY = new Value(0);
    this.pinchEvent = event([{
      nativeEvent: {
        state: pinchState,
        scale: pinchScale,
        focalX: pinchFocalX,
        focalY: pinchFocalY,
      },
    }]);
    const pinchActive = eq(pinchState, State.ACTIVE);

    // The inputs we receive from TapGestureHandler
    const tapState = new Value(-1);
    const tapX = new Value(0);
    const tapY = new Value(0);
    this.tapEvent = event([{
      nativeEvent: {
        state: tapState,
        x: tapX,
        y: tapY,
      },
    }]);

    // The all-important outputs
    const curScale = new Value(1);
    const curX = new Value(0);
    const curY = new Value(0);
    const curOpacity = new Value(1);

    // The centered variables help us know if we need to be recentered
    const recenteredScale = max(curScale, 1);
    const horizontalPanSpace = this.horizontalPanSpace(recenteredScale);
    const verticalPanSpace = this.verticalPanSpace(recenteredScale);

    const resetXClock = new Clock();
    const resetYClock = new Clock();
    const zoomClock = new Clock();

    const dismissingFromPan = new Value(0);

    const roundedCurScale = divide(round(multiply(curScale, 1000)), 1000);
    const gestureActive = or(pinchActive, panActive);
    const activeInteraction = or(
      gestureActive,
      clockRunning(zoomClock),
      dismissingFromPan,
    );

    const updates = [
      this.pinchUpdate(
        pinchActive,
        pinchScale,
        pinchFocalX,
        pinchFocalY,
        curScale,
        curX,
        curY,
      ),
      this.panUpdate(
        panActive,
        panTranslationX,
        panTranslationY,
        curX,
        curY,
      ),
      this.doubleTapUpdate(
        tapState,
        tapX,
        tapY,
        roundedCurScale,
        zoomClock,
        gestureActive,
        curScale,
        curX,
        curY,
      ),
      this.opacityUpdate(
        panState,
        pinchActive,
        panVelocityX,
        panVelocityY,
        curX,
        curY,
        roundedCurScale,
        curOpacity,
        dismissingFromPan,
      ),
      this.recenter(
        resetXClock,
        resetYClock,
        activeInteraction,
        recenteredScale,
        horizontalPanSpace,
        verticalPanSpace,
        curScale,
        curX,
        curY,
      ),
      this.flingUpdate(
        resetXClock,
        resetYClock,
        activeInteraction,
        panVelocityX,
        panVelocityY,
        horizontalPanSpace,
        verticalPanSpace,
        curX,
        curY,
      ),
    ];
    const updatedScale = [ updates, curScale ];
    const updatedCurX = [ updates, curX ];
    const updatedCurY = [ updates, curY ];
    const updatedOpacity = [ updates, curOpacity ];

    const reverseProgress = sub(1, progress);
    this.scale = add(
      multiply(reverseProgress, initialScale),
      multiply(progress, updatedScale),
    );
    this.x = add(
      multiply(reverseProgress, initialTranslateX),
      multiply(progress, updatedCurX),
    );
    this.y = add(
      multiply(reverseProgress, initialTranslateY),
      multiply(progress, updatedCurY),
    );
    this.opacity = multiply(progress, updatedOpacity);
    this.imageContainerOpacity = interpolate(
      progress,
      {
        inputRange: [ 0, 0.1 ],
        outputRange: [ 0, 1 ],
        extrapolate: Extrapolate.CLAMP,
      },
    );
  }

  // How much space do we have to pan the image horizontally?
  horizontalPanSpace(scale: Value) {
    const apparentWidth = multiply(this.imageWidth, scale);
    const horizPop = divide(
      sub(apparentWidth, this.screenWidth),
      2,
    );
    return max(horizPop, 0);
  }

  // How much space do we have to pan the image vertically?
  verticalPanSpace(scale: Value) {
    const apparentHeight = multiply(this.imageHeight, scale);
    const vertPop = divide(
      sub(apparentHeight, this.screenHeight),
      2,
    );
    return max(vertPop, 0);
  }

  pinchUpdate(
    // Inputs
    pinchActive: Value,
    pinchScale: Value,
    pinchFocalX: Value,
    pinchFocalY: Value,
    // Outputs
    curScale: Value,
    curX: Value,
    curY: Value,
  ): Value {
    const deltaScale = scaleDelta(pinchScale, pinchActive);
    const deltaPinchX = multiply(
      sub(1, deltaScale),
      sub(
        pinchFocalX,
        curX,
        this.centerX,
      ),
    );
    const deltaPinchY = multiply(
      sub(1, deltaScale),
      sub(
        pinchFocalY,
        curY,
        this.centerY,
      ),
    );

    return cond(
      [ deltaScale, pinchActive ],
      [
        set(curX, add(curX, deltaPinchX)),
        set(curY, add(curY, deltaPinchY)),
        set(curScale, multiply(curScale, deltaScale)),
      ],
    );
  }

  panUpdate(
    // Inputs
    panActive: Value,
    panTranslationX: Value,
    panTranslationY: Value,
    // Outputs
    curX: Value,
    curY: Value,
  ): Value {
    const deltaX = panDelta(panTranslationX, panActive);
    const deltaY = panDelta(panTranslationY, panActive);
    return cond(
      [ deltaX, deltaY, panActive ],
      [
        set(curX, add(curX, deltaX)),
        set(curY, add(curY, deltaY)),
      ],
    );
  }

  doubleTapUpdate(
    // Inputs
    tapState: Value,
    tapX: Value,
    tapY: Value,
    roundedCurScale: Value,
    zoomClock: Clock,
    gestureActive: Value,
    // Outputs
    curScale: Value,
    curX: Value,
    curY: Value,
  ): Value {
    const zoomClockRunning = clockRunning(zoomClock);
    const zoomActive = and(not(gestureActive), zoomClockRunning);
    const targetScale = cond(greaterThan(roundedCurScale, 1), 1, 3);

    const tapXDiff = sub(tapX, this.centerX, curX);
    const tapYDiff = sub(tapY, this.centerY, curY);
    const tapXPercent = divide(tapXDiff, this.imageWidth, curScale);
    const tapYPercent = divide(tapYDiff, this.imageHeight, curScale);

    const horizPanSpace = this.horizontalPanSpace(targetScale);
    const vertPanSpace = this.verticalPanSpace(targetScale);
    const horizPanPercent = divide(horizPanSpace, this.imageWidth, targetScale);
    const vertPanPercent = divide(vertPanSpace, this.imageHeight, targetScale);

    const tapXPercentClamped = clamp(
      tapXPercent,
      multiply(-1, horizPanPercent),
      horizPanPercent,
    );
    const tapYPercentClamped = clamp(
      tapYPercent,
      multiply(-1, vertPanPercent),
      vertPanPercent,
    );
    const targetX = multiply(tapXPercentClamped, this.imageWidth, targetScale);
    const targetY = multiply(tapYPercentClamped, this.imageHeight, targetScale);

    const targetRelativeScale = divide(targetScale, curScale);
    const targetRelativeX = multiply(-1, add(targetX, curX));
    const targetRelativeY = multiply(-1, add(targetY, curY));

    const zoomScale = runTiming(zoomClock, 1, targetRelativeScale);
    const zoomX = runTiming(zoomClock, 0, targetRelativeX, false);
    const zoomY = runTiming(zoomClock, 0, targetRelativeY, false);

    const deltaScale = scaleDelta(zoomScale, zoomActive);
    const deltaX = panDelta(zoomX, zoomActive);
    const deltaY = panDelta(zoomY, zoomActive);

    const tapJustEnded = gestureJustEnded(tapState);

    return cond(
      [ tapJustEnded, deltaX, deltaY, deltaScale, gestureActive ],
      stopClock(zoomClock),
      cond(
        or(zoomClockRunning, tapJustEnded),
        [
          zoomX,
          zoomY,
          zoomScale,
          set(curX, add(curX, deltaX)),
          set(curY, add(curY, deltaY)),
          set(curScale, multiply(curScale, deltaScale)),
        ],
      ),
    );
  }

  opacityUpdate(
    // Inputs
    panState: Value,
    pinchActive: Value,
    panVelocityX: Value,
    panVelocityY: Value,
    curX: Value,
    curY: Value,
    roundedCurScale: Value,
    // Outputs
    curOpacity: Value,
    dismissingFromPan: Value,
  ): Value {
    const progressiveOpacity = max(
      min(
        sub(1, abs(divide(curX, this.screenWidth))),
        sub(1, abs(divide(curY, this.screenHeight))),
      ),
      0,
    );
    const panJustEnded = gestureJustEnded(panState);

    const resetClock = new Clock();

    const velocity = pow(add(pow(panVelocityX, 2), pow(panVelocityY, 2)), 0.5);
    const shouldGoBack = and(
      panJustEnded,
      or(
        greaterThan(velocity, 50),
        greaterThan(0.7, progressiveOpacity),
      ),
    );

    const decayClock = new Clock();
    const decay = [
      set(curX, runDecay(decayClock, panVelocityX, curX, false)),
      set(curY, runDecay(decayClock, panVelocityY, curY)),
    ];

    return cond(
      [ panJustEnded, dismissingFromPan ],
      decay,
      cond(
        or(pinchActive, greaterThan(roundedCurScale, 1)),
        set(curOpacity, runTiming(resetClock, curOpacity, 1)),
        [
          stopClock(resetClock),
          set(curOpacity, progressiveOpacity),
          set(dismissingFromPan, shouldGoBack),
          cond(
            shouldGoBack,
            [
              decay,
              call([], this.close),
            ],
          ),
        ],
      ),
    );
  }

  recenter(
    // Inputs
    resetXClock: Clock,
    resetYClock: Clock,
    activeInteraction: Value,
    recenteredScale: Value,
    horizontalPanSpace: Value,
    verticalPanSpace: Value,
    // Outputs
    curScale: Value,
    curX: Value,
    curY: Value,
  ): Value {
    const resetScaleClock = new Clock();

    const recenteredX = clamp(
      curX,
      multiply(-1, horizontalPanSpace),
      horizontalPanSpace,
    );
    const recenteredY = clamp(
      curY,
      multiply(-1, verticalPanSpace),
      verticalPanSpace,
    );

    return cond(
      activeInteraction,
      [
        stopClock(resetScaleClock),
        stopClock(resetXClock),
        stopClock(resetYClock),
      ],
      [
        cond(
          or(
            clockRunning(resetScaleClock),
            neq(recenteredScale, curScale),
          ),
          set(curScale, runTiming(resetScaleClock, curScale, recenteredScale)),
        ),
        cond(
          or(
            clockRunning(resetXClock),
            neq(recenteredX, curX),
          ),
          set(curX, runTiming(resetXClock, curX, recenteredX)),
        ),
        cond(
          or(
            clockRunning(resetYClock),
            neq(recenteredY, curY),
          ),
          set(curY, runTiming(resetYClock, curY, recenteredY)),
        ),
      ],
    );
  }

  flingUpdate(
    // Inputs
    resetXClock: Clock,
    resetYClock: Clock,
    activeInteraction: Value,
    panVelocityX: Value,
    panVelocityY: Value,
    horizontalPanSpace: Value,
    verticalPanSpace: Value,
    // Outputs
    curX: Value,
    curY: Value,
  ): Value {
    const flingXClock = new Clock();
    const flingYClock = new Clock();

    const decayX = runDecay(flingXClock, panVelocityX, curX);
    const recenteredX = clamp(
      decayX,
      multiply(-1, horizontalPanSpace),
      horizontalPanSpace,
    );
    const decayY = runDecay(flingYClock, panVelocityY, curY);
    const recenteredY = clamp(
      decayY,
      multiply(-1, verticalPanSpace),
      verticalPanSpace,
    );

    return cond(
      activeInteraction,
      [
        stopClock(flingXClock),
        stopClock(flingYClock),
      ],
      [
        set(curX, recenteredX),
        set(curY, recenteredY),
        cond(
          or(
            clockRunning(resetXClock),
            neq(decayX, recenteredX),
          ),
          stopClock(flingXClock),
        ),
        cond(
          or(
            clockRunning(resetYClock),
            neq(decayY, recenteredY),
          ),
          stopClock(flingYClock),
        ),
      ],
    );
  }

  updateDimensions() {
    this.screenWidth.setValue(width);
    this.screenHeight.setValue(height);

    this.centerX.setValue(width / 2);
    this.centerY.setValue(height / 2);

    this.imageWidth.setValue(width);
    this.imageHeight.setValue(height);
  }

  componentDidUpdate(prevProps: Props) {
    if (
      this.props.screenDimensions !== prevProps.screenDimensions ||
      this.props.contentVerticalOffset !== prevProps.contentVerticalOffset
    ) {
      this.updateDimensions();
    }
  }

  get screenDimensions(): Dimensions {
    return { width, height }
  }

  get imageDimensions(): Dimensions {
    return { width, height }
  }

  get imageContainerStyle() {
    const { height, width } = this.imageDimensions;
    const { height: screenHeight, width: screenWidth } = this.screenDimensions;
    const top = (screenHeight - height) / 2;
    const left = (screenWidth - width) / 2;
    return {
      height,
      width,
      marginTop: top,
      marginLeft: left,
      opacity: this.imageContainerOpacity,
    };
  }

  static isActive(props) {
    return true;
  }

  get contentContainerStyle() {
    const fullScreenHeight = this.screenDimensions.height;
    const top = 0;
    const bottom = fullScreenHeight;

    // margin will clip, but padding won't
    const verticalStyle = MultimediaModal.isActive(this.props)
      ? { paddingTop: top, paddingBottom: bottom }
      : { marginTop: top, marginBottom: bottom };
    return [ styles.contentContainer, verticalStyle ];
  }

  render() {
    const statusBar = null;
    const backdropStyle = { opacity: this.opacity };
    const closeButtonStyle = {
      opacity: this.opacity,
      top: 4,
    };
    const view = (
      <Animated.View style={styles.container}>
        {statusBar}
        <Animated.View style={[ styles.backdrop, backdropStyle ]} />
        <View style={this.contentContainerStyle}>
          <Animated.View style={this.imageContainerStyle}>
            <Svg
              width={width}
              height={height}
              viewBox={`${origin} ${origin * ratio} ${width} ${width}`}
            >
              <AnimatedG
                style={{
                  transform: [
                    { translateX: this.x },
                    { translateY: this.y },
                    { translateX: oX },
                    { scale: this.scale },
                    { translateX: -oX },
                  ],
                }}
              >
                <G transform={`translate(${origin},${origin})`}>
                  <Rect
                    x={0}
                    y={0}
                    width={width}
                    height={width}
                    fill={fill()}
                  />
                  <Polygons />
                </G>
              </AnimatedG>
            </Svg>
          </Animated.View>
        </View>
        <Animated.View style={[
          styles.closeButtonContainer,
          closeButtonStyle,
        ]}>
          <TouchableOpacity onPress={this.close}>
            <Text style={styles.closeButton}>
              ×
            </Text>
          </TouchableOpacity>
        </Animated.View>
      </Animated.View>
    );
    return (
      <PinchGestureHandler
        onGestureEvent={this.pinchEvent}
        onHandlerStateChange={this.pinchEvent}
        simultaneousHandlers={this.handlerRefs}
        ref={this.pinchHandler}
      >
        <Animated.View style={styles.container}>
          <PanGestureHandler
            onGestureEvent={this.panEvent}
            onHandlerStateChange={this.panEvent}
            simultaneousHandlers={this.handlerRefs}
            ref={this.panHandler}
            avgTouches
          >
            <Animated.View style={styles.container}>
              <TapGestureHandler
                onHandlerStateChange={this.tapEvent}
                simultaneousHandlers={this.handlerRefs}
                ref={this.tapHandler}
                waitFor={this.priorityHandlerRefs}
                numberOfTaps={2}
              >
                {view}
              </TapGestureHandler>
            </Animated.View>
          </PanGestureHandler>
        </Animated.View>
      </PinchGestureHandler>
    );
  }

  close = () => {
  }

}

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  backdrop: {
    position: "absolute",
    top: 0,
    bottom: 0,
    left: 0,
    right: 0,
    backgroundColor: "white",
  },
  contentContainer: {
    flex: 1,
    overflow: "hidden",
  },
  closeButtonContainer: {
    position: "absolute",
    right: 4,
  },
  closeButton: {
    paddingTop: 2,
    paddingBottom: 2,
    paddingLeft: 8,
    paddingRight: 8,
    fontSize: 36,
    color: "white",
    textShadowColor: "#000",
    textShadowOffset: { width: 0, height: 1 },
    textShadowRadius: 1,
  },
});

export default MultimediaModal

const xy = 0.9 * width;
const side = 0.1 * width;
const origin = -width / 2;
const ratio = width / height;
const oX = android ? origin : 0;

const pos = () => Math.random() * xy;
const byte = () => Math.floor(Math.random() * 256);
const fill = () => `rgba(${byte()}, ${byte()}, ${byte()}, 0.2)`;

const polygons = Array.from({ length: 200 }).map((_, i) => (
  <Rect
    key={i}
    x={pos()}
    y={pos()}
    width={side}
    height={side}
    fill={fill()}
    stroke="black"
  />
));

const Polygons = memo(() => polygons, () => true);