software-mansion/react-native-svg

animateTransform property does not work in RN iOS

Opened this issue · 12 comments

B4R05 commented

Only building an iOS app, so can't talk for Android, but when the following is run, the svg appears but not the animation. I have tried importing the animateTransform property too, still not working.

  <Svg
    width="38px"
    height="38px"
    viewBox="0 0 38 38"
    xmlns="http://www.w3.org/2000/svg"
    stroke="blue"
    aria-label="spinner"
  >
    <G fill="none" fillRule="evenodd">
      <G transform="translate(1 1)" strokeWidth="2">
        <Circle strokeOpacity=".5" cx="18" cy="18" r="18" />
        <Path d="M36 18c0-9.94-8.06-18-18-18">
          <animateTransform
            attributeName="transform"
            type="rotate"
            from="0 18 18"
            to="360 18 18"
            dur="1s"
            repeatCount="indefinite"
          />
        </Path>
      </G>
    </G>
  </svg>
msand commented

The animation elements from the svg spec aren't supported yet. Instead we support using Animated from react-native: #180 (comment)
#180 (comment)

kopax commented

Hi, I am also experiencing this issue, is there no web/native mapping for this? Should I rewrite the effect with the animated ? (2020)

msand commented

Yeah, would be great if you could make a pr with some elements that translate svg animations to animated.

kopax commented

Yes I understand that. I must be aware of all the change I am facing doing a react native application but I am aware this project needs more contributors. So far I keep having a domino effect. (if I touch something are add something, everything fall). I must increase my velocity to get back to normal and I'll try uses some free time helping the react-native community (svg is not my cup of tee TBH)

msand commented

It's relatively easy to write a function which translates the animate syntax to animated, e.g. like this: https://snack.expo.io/@msand/animatetransform

import * as React from 'react';
import { Animated, Easing, PanResponder, View } from 'react-native';
import { Svg, Circle, G, Path } from 'react-native-svg';
const AnimatedPath = Animated.createAnimatedComponent(Path);
const AnimatedCircle = Animated.createAnimatedComponent(Circle);
const AnimatedSvg = Animated.createAnimatedComponent(Svg);
/*
  <Svg
    width="38px"
    height="38px"
    viewBox="0 0 38 38"
    xmlns="http://www.w3.org/2000/svg"
    stroke="blue"
    aria-label="spinner"
  >
    <G fill="none" fillRule="evenodd">
      <G transform="translate(1 1)" strokeWidth="2">
        <Circle strokeOpacity=".5" cx="18" cy="18" r="18" />
        <Path d="M36 18c0-9.94-8.06-18-18-18">
          <animateTransform
            attributeName="transform"
            type="rotate"
            from="0 18 18"
            to="360 18 18"
            dur="1s"
            repeatCount="indefinite"
          />
        </Path>
      </G>
    </G>
  </svg>
*/
function animateTransform({ type, from, to, dur, repeatCount }) {
  const duration = parseFloat(dur.slice(0, -1)) * 1000;
  const [fromAngle, fromCX, fromCY] = from.split(' ').map(Number);
  const [toAngle, toCX, toCY] = to.split(' ').map(Number);
  const t = new Animated.Value(0);
  const animateTransform = [
    Animated.timing(t, {
      duration,
      toValue: 1,
      useNativeDriver: true,
      easing: Easing.linear,
    }),
  ];
  const animation = Animated.loop(Animated.sequence(animateTransform), {
    iterations: -1,
  }).start();
  const rotateAngle = t.interpolate({
    inputRange: [0, 1],
    outputRange: [fromAngle + 'deg', toAngle + 'deg'],
  });
  const cx = t.interpolate({
    inputRange: [0, 1],
    outputRange: [fromCX, toCX],
  });
  const cy = t.interpolate({
    inputRange: [0, 1],
    outputRange: [fromCY, toCY],
  });
  const icx = t.interpolate({
    inputRange: [0, 1],
    outputRange: [-fromCX, -toCX],
  });
  const icy = t.interpolate({
    inputRange: [0, 1],
    outputRange: [-fromCY, -toCY],
  });
  const style = {
    transform: [
      { translateX: cx },
      { translateY: cy },
      { rotateZ: rotateAngle },
      { translateX: icx },
      { translateY: icy },
    ],
  };
  return { t, animation, style, rotateAngle, cx, cy, icx, icy };
}

export default () => {
  const { style } = animateTransform({
    type: 'rotate',
    from: '0 18 18',
    to: '360 18 18',
    dur: '1s',
    repeatCount: 'indefinite',
  });
  return (
    <View>
      <Svg
        width="100%"
        height="50%"
        viewBox="0 0 38 38"
        aria-label="spinner"
        fillRule="evenodd"
        stroke="blue"
        fill="none">
        <G transform="translate(1 1)" strokeWidth="2">
          <Circle strokeOpacity=".5" cx="18" cy="18" r="18" />
          <AnimatedPath d="M36 18c0-9.94-8.06-18-18-18" style={style} />
        </G>
      </Svg>
      <App2 />
    </View>
  );
};

function animateSpline({
  values,
  dur,
  repeatCount,
  begin,
  keyTimes,
  keySplines,
}) {
  const duration = dur * 1000;
  const t = new Animated.Value(keyTimes[0]);
  const splines = keySplines.map((spline, i) => {
    const [x1, y1, x2, y2] = spline;
    const fromValue = keyTimes[i];
    const toValue = keyTimes[i + 1];
    return Animated.timing(t, {
      toValue,
      delay: i == 0 ? begin : 0,
      duration: duration * (toValue - fromValue),
      easing: Easing.bezier(x1, y1, x2, y2),
      useNativeDriver: true,
    });
  });
  const iterations = repeatCount === 'indefinite' ? -1 : +repeatCount;
  const animation = Animated.loop(Animated.sequence(splines), { iterations });
  const value = t.interpolate({
    inputRange: keyTimes,
    outputRange: values,
  });
  return { t, animation, value, splines };
}

function panHandler() {
  const x = new Animated.Value(0);
  const y = new Animated.Value(0);
  const dx = new Animated.Value(0);
  const dy = new Animated.Value(0);

  const panResponder = PanResponder.create({
    // Ask to be the responder:
    onStartShouldSetPanResponder: (evt, gestureState) => true,
    onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
    onMoveShouldSetPanResponder: (evt, gestureState) => true,
    onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,

    // The gesture has started. Show visual feedback so the user knows
    // what is happening!
    // gestureState.d{x,y} will be set to zero now
    onPanResponderGrant: (evt, gestureState) => {},

    // The most recent move distance is gestureState.move{X,Y}
    // The accumulated gesture distance since becoming responder is
    // gestureState.d{x,y}
    onPanResponderMove: Animated.event([
      null, // ignore the native event
      // extract dx and dy from gestureState
      // like 'pan.x = gestureState.dx, pan.y = gestureState.dy'
      { dx, dy },
    ]),

    onPanResponderTerminationRequest: (evt, gestureState) => true,
    // The user has released all touches while this view is the
    // responder. This typically means a gesture has succeeded
    onPanResponderRelease: (evt, gestureState) => {
      x.setValue(x._value + gestureState.dx);
      y.setValue(y._value + gestureState.dy);
      dx.setValue(0);
      dy.setValue(0);
    },
    // Another component has become the responder, so this gesture
    // should be cancelled
    onPanResponderTerminate: () => {},

    // Returns whether this component should block native components from becoming the JS
    // responder. Returns true by default. Is currently only supported on android.
    onShouldBlockNativeResponder: (evt, gestureState) => {
      return true;
    },
  });

  return {
    x,
    y,
    dx,
    dy,
    panResponder,
    translateX: Animated.add(x, dx),
    translateY: Animated.add(y, dy),
  };
}

export function App2() {
  /*
    <circle cx="16" cy="16" r="16">
      <animate 
        attributeName="r" 
        values="0; 4; 0; 0" 
        dur="1.2s" 
        repeatCount="indefinite" 
        begin="0" 
        keytimes="0;0.2;0.7;1" 
        keySplines="0.2 0.2 0.4 0.8;0.2 0.6 0.4 0.8;0.2 0.6 0.4 0.8" 
        calcMode="spline" />
    </circle>
  */
  const { animation, value } = animateSpline({
    values: [0, 4, 0, 0],
    dur: 1.2,
    repeatCount: 'indefinite',
    begin: 0,
    keyTimes: [0, 0.2, 0.7, 1],
    keySplines: [
      [0.2, 0.2, 0.4, 0.8],
      [0.2, 0.6, 0.4, 0.8],
      [0.2, 0.6, 0.4, 0.8],
    ],
  });
  animation.start();

  const { panResponder, translateX, translateY } = panHandler();

  return (
    <View {...panResponder.panHandlers}>
      <AnimatedSvg
        width="100%"
        height="100%"
        viewBox="0 0 32 32"
        style={{
          transform: [{ translateX }, { translateY }],
        }}>
        <AnimatedCircle cx="16" cy="16" r={value} />
      </AnimatedSvg>
    </View>
  );
}
kopax commented

Thanks for sharing, I will study it, thanks and best!

@msand

In your expo example, animations don't work on Android and iOS, only the web version works fine. I didn't test it on a real device though.

I just test it on my android emulator, it seems ratateZ can not be animated by setting useNativeDriver and I'm getting a runtime error.

by the way, the generate animations going to be very slow when more SVG components exist on the screen. is it possible to animate the components using react-native-reanimated library? have you tried it before yourself and if yes how much the performance improved?

stale commented

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. You may also mark this issue as a "discussion" and I will leave this open.

stale commented

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. You may also mark this issue as a "discussion" and I will leave this open.

stale commented

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. You may also mark this issue as a "discussion" and I will leave this open.

@msand so does the last example you provided work? is it added to the library?