Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Rect] width not being animated via createAnimatedComponent and interpolate #803

Closed
xzilja opened this issue Oct 6, 2018 · 30 comments
Closed

Comments

@xzilja
Copy link

xzilja commented Oct 6, 2018

I have simple animation interpolation going on for <Rect /> element, however I see no changes on my ui, as if width is staying 0.

I manually added my end value to the component as width={149.12} and it displayed it correctly, hence I am a bit confused now to why it is not picking up same value from animation?

react-native@0.57.1
react-native-svg@7.0.3
iOS

Here is full implementation, in essence a mana and health bar that take in current value and total value i.e. 50 and 100 should display half width for the rect. (Example uses typescript)

import * as React from 'react'
import { Animated } from 'react-native'
import Svg, { Defs, LinearGradient, Rect, Stop } from 'react-native-svg'
import { deviceWidth } from '../services/Device'

const barWidth = deviceWidth * 0.3454
const barHeight = barWidth * 0.093
const AnimatedRect = Animated.createAnimatedComponent(Rect)

/**
 * Types
 */
export interface IProps {
  variant: 'MANA' | 'HEALTH'
  currentValue: number
  totalValue: number
}

export interface IState {
  width: Animated.Value
}

/**
 * Component
 */
class HealthManaBar extends React.Component<IProps, IState> {
  state = {
    width: new Animated.Value(0)
  }

  rectangleRef = React.createRef()

  componentDidMount() {
    const { currentValue, totalValue } = this.props
    this.animate(currentValue, totalValue)
  }

  componentDidUpdate({ currentValue, totalValue }: IProps) {
    this.animate(currentValue, totalValue)
  }

  animate = (current: number, total: number) =>
    Animated.timing(this.state.width, {
      toValue: current !== 0 ? current / total : 0,
      duration: 400,
      useNativeDriver: false
    }).start()

  render() {
    const { variant } = this.props
    const { width } = this.state

    return (
      <Svg width={barWidth} height={barHeight}>
        <Defs>
          <LinearGradient
            id={`HeathManaBar-gradient-${variant}`}
            x1="0"
            y1="0"
            x2="0"
            y2={barHeight}
          >
            <Stop
              offset="0"
              stopColor={variant === 'HEALTH' ? '#EC561B' : '#00ACE1'}
              stopOpacity="1"
            />
            <Stop
              offset="0.5"
              stopColor={variant === 'HEALTH' ? '#8D1B00' : '#003FAA'}
              stopOpacity="1"
            />
            <Stop
              offset="1"
              stopColor={variant === 'HEALTH' ? '#9F3606' : '#007C97'}
              stopOpacity="1"
            />
          </LinearGradient>
        </Defs>
        <AnimatedRect
          x="0"
          y="0"
          rx="3"
          ry="3"
          width={width.interpolate({
            inputRange: [0, 1],
            outputRange: [0, barWidth]
          })}
          height={barHeight}
          fill={`url(#HeathManaBar-gradient-${variant})`}
        />
      </Svg>
    )
  }
}

export default HealthManaBar

NOTE: this is based on following example https://github.com/msand/SVGPodTest/blob/a74a600818e496efaa78298291b63107295064bf/App.js#L14-L57

Only difference I see is that it uses strings '0' and '50', which I also tried, but got react native error saying that NSString can't be converted to Yoga Value. However, when I pass width to my <Rect /> as an integer it works correctly, so I assume this shouldn't matter as much in v7?

@xzilja xzilja changed the title [Rect] width not being animated [Rect] width not being animated via createAnimatedComponent Oct 6, 2018
@xzilja xzilja changed the title [Rect] width not being animated via createAnimatedComponent [Rect] width not being animated via createAnimatedComponent and interpolate Oct 7, 2018
@msand
Copy link
Collaborator

msand commented Oct 9, 2018

This seems to work fine at least:
https://snack.expo.io/@msand/animate-rect-width

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

const { Rect } = Svg;

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

class SvgRoot extends Component {
  state = {
    initAnim: new Animated.Value(0),
  };

  componentDidMount() {
    Animated.timing(
      // Animate over time
      this.state.initAnim,
      {
        toValue: 1,
        duration: 3000,
        useNativeDriver: false,
      }
    ).start();
  }

  render() {
    const { initAnim } = this.state;
    let animateWidth = initAnim.interpolate({
      inputRange: [0, 1],
      outputRange: ['0', '80'],
    });
    return (
      <AnimatedSvg width={width} height={height} viewBox="0 0 100 100">
        <AnimatedRect
          y="10"
          x="10"
          height="80"
          width={animateWidth}
        />
      </AnimatedSvg>
    );
  }
}

export default class App extends Component {
  render() {
    return (
      <View style={styles.container}>
        <SvgRoot />
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    backgroundColor: '#ecf0f1',
  },
});

@xzilja
Copy link
Author

xzilja commented Oct 9, 2018

@msand I'm not sure, but isn't expo still on v6 of react-native-svg? (double checked this they are using v 6.2.2 as of now)

Also

    let animateWidth = initAnim.interpolate({
      inputRange: [0, 1],
      outputRange: ['0', '80'],
    });

You used strings here for output range, which in rn 0.57.x yields error

NSString can't be converted to Yoga Value

I think with v7 we can use numbers here? At least I am able to use <Rect width={80} />


I see you also made SVG an animated component, I will try that out, but is there a reason why it needs to be one?

@xzilja
Copy link
Author

xzilja commented Oct 9, 2018

UPDATE: Making Svg animated as well in my example didn't do the trick :/

@msand
Copy link
Collaborator

msand commented Oct 9, 2018

Do you have useNativeDriver: false?

@xzilja
Copy link
Author

xzilja commented Oct 9, 2018

@msand I didn't, but added it just in case and tested, same result. I added it to code in my question to reflect current state. I think useNativeDriver should be "undefined" by default anyway however.

I also tried animating width of Svg element instead of rect, no luck there either :/

I'm not sure if it helps at all, but I added ref to my AnimatedRect its width doesn't seem to be a number or string, rather animated interpolation function, not sure if this is causing issue possibly?

screenshot 2018-10-09 at 14 56 01

@msand
Copy link
Collaborator

msand commented Oct 10, 2018

Please try with the latest commit from the master branch, it should work both with and without native driver now, at least this code seems to work fine:

import React from "react";
import { Dimensions, Animated } from "react-native";
import Svg, { Defs, LinearGradient, Rect, Stop } from "react-native-svg";

const { width, height } = Dimensions.get("window");

const barWidth = width * 0.3454;
const barHeight = barWidth * 0.093;
const AnimatedRect = Animated.createAnimatedComponent(Rect);

/**
 * Component
 */
class HealthManaBar extends React.Component {
  state = {
    width: new Animated.Value(0),
  };

  rectangleRef = React.createRef();

  componentDidMount() {
    const { currentValue, totalValue } = this.props;
    this.animate(currentValue, totalValue);
  }

  componentDidUpdate({ currentValue, totalValue }) {
    this.animate(currentValue, totalValue);
  }

  animate = (current, total) =>
    Animated.timing(this.state.width, {
      toValue: current / total,
      duration: 4000,
      useNativeDriver: true,
    }).start();

  render() {
    const { variant } = this.props;
    const { width } = this.state;

    return (
      <Svg width={barWidth} height={barHeight}>
        <Defs>
          <LinearGradient
            id={`HeathManaBar-gradient-${variant}`}
            x1="0"
            y1="0"
            x2="0"
            y2={barHeight}
          >
            <Stop
              offset="0"
              stopColor={variant === "HEALTH" ? "#EC561B" : "#00ACE1"}
              stopOpacity="1"
            />
            <Stop
              offset="0.5"
              stopColor={variant === "HEALTH" ? "#8D1B00" : "#003FAA"}
              stopOpacity="1"
            />
            <Stop
              offset="1"
              stopColor={variant === "HEALTH" ? "#9F3606" : "#007C97"}
              stopOpacity="1"
            />
          </LinearGradient>
        </Defs>
        <AnimatedRect
          x="0"
          y="0"
          rx="3"
          ry="3"
          rectwidth={width.interpolate({
            inputRange: [0, 1],
            outputRange: [0, barWidth],
          })}
          width={width.interpolate({
            inputRange: [0, 1],
            outputRange: [0, barWidth],
          })}
          height={barHeight}
          fill={`url(#HeathManaBar-gradient-${variant})`}
        />
      </Svg>
    );
  }
}

export default function App() {
  return <HealthManaBar totalValue={100} currentValue={50} variant="HEALTH" />;
}

@adam-s
Copy link

adam-s commented Oct 10, 2018

I'm having the same problem with props x, y on the Text, G, and TSpan components.

@msand
Copy link
Collaborator

msand commented Oct 10, 2018

It requires replicating these changes to those components and properties:

ios: 7c012a9

android:
the changes in
android/src/main/java/com/horcrux/svg/RectShadowNode.java
and
android/src/main/java/com/horcrux/svg/RenderableViewManager.java
e307eee#diff-f992420cdd73af55d1a573a979adec6eR16

@adam-s Could you attempt making the changes and open a pr?

@msand
Copy link
Collaborator

msand commented Oct 11, 2018

@adam-s I realized it requires some other changes in the js side as well: 2a43579
Now I have support for animation of transforms, x, y etc on G, Text and TSpan at least with useNativeDriver: false
Try something like this:

import React from "react";
import { Dimensions, Animated } from "react-native";
import Svg, { Defs, LinearGradient, Rect, Stop, Text, TSpan, G } from "react-native-svg";

const { width, height } = Dimensions.get("window");

const barWidth = width * 0.5;
const barHeight = barWidth * 0.5;
const AnimatedRect = Animated.createAnimatedComponent(Rect);
const AnimatedText = Animated.createAnimatedComponent(Text);
const AnimatedTSpan = Animated.createAnimatedComponent(TSpan);
const AnimatedG = Animated.createAnimatedComponent(G);

/**
 * Component
 */
class HealthManaBar extends React.Component {
  state = {
    width: new Animated.Value(0),
  };

  rectangleRef = React.createRef();

  componentDidMount() {
    const { currentValue, totalValue } = this.props;
    this.animate(currentValue, totalValue);
  }

  componentDidUpdate({ currentValue, totalValue }) {
    this.animate(currentValue, totalValue);
  }

  animate = (current, total) =>
    Animated.timing(this.state.width, {
      toValue: current / total,
      duration: 4000,
      useNativeDriver: false,
    }).start();

  render() {
    const { variant } = this.props;
    const { width } = this.state;

    return (
      <Svg width={barWidth} height={barHeight}>
        <Defs>
          <LinearGradient
            id={`HeathManaBar-gradient-${variant}`}
            x1="0"
            y1="0"
            x2="0"
            y2={barHeight}
          >
            <Stop
              offset="0"
              stopColor={variant === "HEALTH" ? "#EC561B" : "#00ACE1"}
              stopOpacity="1"
            />
            <Stop
              offset="0.5"
              stopColor={variant === "HEALTH" ? "#8D1B00" : "#003FAA"}
              stopOpacity="1"
            />
            <Stop
              offset="1"
              stopColor={variant === "HEALTH" ? "#9F3606" : "#007C97"}
              stopOpacity="1"
            />
          </LinearGradient>
        </Defs>
        <AnimatedRect
          x="0"
          y="0"
          rx="3"
          ry="3"
          rectwidth={width.interpolate({
            inputRange: [0, 1],
            outputRange: [0, barWidth],
          })}
          width={width.interpolate({
            inputRange: [0, 1],
            outputRange: [0, barWidth],
          })}
          height={barHeight}
          fill={`url(#HeathManaBar-gradient-${variant})`}
        />
        <AnimatedG
          x={width.interpolate({
            inputRange: [0, 1],
            outputRange: [0, barWidth],
          })}
        >
          <AnimatedText y={width.interpolate({
            inputRange: [0, 1],
            outputRange: ['0', `${barHeight}`],
          })}
          >
            <AnimatedTSpan x={width.interpolate({
              inputRange: [0, 2],
              outputRange: ['0', `${barWidth}`],
            })}
            >
            Test
            </AnimatedTSpan>
          </AnimatedText>
        </AnimatedG>
      </Svg>
    );
  }
}

export default function App() {
  return <HealthManaBar totalValue={100} currentValue={50} variant="HEALTH" />;
}

@msand
Copy link
Collaborator

msand commented Oct 11, 2018

@adam-s managed to get useNativeDriver: true animation of x and y properties on Text and TSpan now as well: a87096d

import React from "react";
import { Dimensions, Animated } from "react-native";
import Svg, {
  Defs,
  LinearGradient,
  Rect,
  Stop,
  Text,
  TSpan,
  G,
} from "react-native-svg";

const { width, height } = Dimensions.get("window");

const barWidth = width * 0.5;
const barHeight = barWidth * 0.5;
const AnimatedRect = Animated.createAnimatedComponent(Rect);
const AnimatedText = Animated.createAnimatedComponent(Text);
const AnimatedTSpan = Animated.createAnimatedComponent(TSpan);
const AnimatedG = Animated.createAnimatedComponent(G);

/**
 * Component
 */
class HealthManaBar extends React.Component {
  state = {
    width: new Animated.Value(0),
  };

  rectangleRef = React.createRef();

  componentDidMount() {
    const { currentValue, totalValue } = this.props;
    this.animate(currentValue, totalValue);
  }

  componentDidUpdate({ currentValue, totalValue }) {
    this.animate(currentValue, totalValue);
  }

  animate = (current, total) =>
    Animated.timing(this.state.width, {
      toValue: current / total,
      duration: 4000,
      useNativeDriver: true,
    }).start();

  render() {
    const { variant } = this.props;
    const { width } = this.state;

    return (
      <Svg width={barWidth} height={barHeight}>
        <Defs>
          <LinearGradient
            id={`HeathManaBar-gradient-${variant}`}
            x1="0"
            y1="0"
            x2="0"
            y2={barHeight}
          >
            <Stop
              offset="0"
              stopColor={variant === "HEALTH" ? "#EC561B" : "#00ACE1"}
              stopOpacity="1"
            />
            <Stop
              offset="0.5"
              stopColor={variant === "HEALTH" ? "#8D1B00" : "#003FAA"}
              stopOpacity="1"
            />
            <Stop
              offset="1"
              stopColor={variant === "HEALTH" ? "#9F3606" : "#007C97"}
              stopOpacity="1"
            />
          </LinearGradient>
        </Defs>
        <AnimatedRect
          x="0"
          y="0"
          rx="3"
          ry="3"
          rectwidth={width.interpolate({
            inputRange: [0, 1],
            outputRange: [0, barWidth],
          })}
          width={width.interpolate({
            inputRange: [0, 1],
            outputRange: [0, barWidth],
          })}
          height={barHeight}
          fill={`url(#HeathManaBar-gradient-${variant})`}
        />
        <G>
          <AnimatedText
            positionY={width.interpolate({
              inputRange: [0, 1],
              outputRange: [0, barHeight],
            })}
            y={width.interpolate({
              inputRange: [0, 1],
              outputRange: [0, barHeight],
            })}
          >
            <AnimatedTSpan
              positionX={width.interpolate({
                inputRange: [0, 2],
                outputRange: [0, barWidth],
              })}
              x={width.interpolate({
                inputRange: [0, 2],
                outputRange: [0, barWidth],
              })}
            >
              Test
            </AnimatedTSpan>
          </AnimatedText>
        </G>
      </Svg>
    );
  }
}

export default function App() {
  return <HealthManaBar totalValue={100} currentValue={50} variant="HEALTH" />;
}

@adam-s
Copy link

adam-s commented Oct 11, 2018

There is a PanResponder in a different component with a class that contains a lot of D3 logic. All I'm looking for is the text to be on the same x coordinate as the touch event. It works with Circle, Line, Animated.View, and Animated.Text. (Yes, I know it isn't very 'reactive'. It is performant.)

import React from 'react';
import PropTypes from 'prop-types';
import { Animated } from 'react-native';
import { Text } from 'react-native-svg';

import CCColors from '../../common/components/CCColors';

const AnimatedText = Animated.createAnimatedComponent(Text);

const textStyle = {
  fill: CCColors.slate,
  fontFamily: "'Gill Sans', 'Gill Sans MT', 'Seravek', 'Trebuchet MS', sans-serif",
  fontSize: 14,
  letterSpacing: 'normal',
  stroke: 'transparent',
  textAnchor: 'middle',
};

const propTypes = {
  addListener: PropTypes.func.isRequired,
  removeListener: PropTypes.func.isRequired,
};
const defaultProps = {};

class HistoricalTooltip extends React.Component {
  constructor(props) {
    super(props);
    this.callback = this.callback.bind(this);
  }
  componentDidMount() {
    const { addListener } = this.props;
    addListener(this.callback);
  }
  componentWillUnmount() {
    const { removeListener } = this.props;
    removeListener(this.callback);
  }
  setRef(ref) {
    this.ref = ref;
  }
  callback(data) {
    const { x } = data;
    this.ref.setNativeProps({ x });
  }
  render() {
    return (
      <AnimatedText
        ref={ref => this.setRef(ref)}
        x={100}
        y={100}
      >
      Hello
      </AnimatedText>
    );
  }
}

HistoricalTooltip.propTypes = propTypes;
HistoricalTooltip.defaultProps = defaultProps;

export default HistoricalTooltip;

@msand
Copy link
Collaborator

msand commented Oct 11, 2018

Now with support for useNativeDriver with transform styles using the same syntax as for react-native views: fb4e877

import React from "react";
import { Dimensions, Animated } from "react-native";
import Svg, { Text, TSpan, G } from "react-native-svg";

const { width, height } = Dimensions.get("window");

const AnimatedG = Animated.createAnimatedComponent(G);

class NativeAnimGTransform extends React.Component {
  state = {
    anim: new Animated.Value(0),
  };

  componentDidMount() {
    this.animate(this.props.value);
  }

  componentDidUpdate({ value }) {
    this.animate(value);
  }

  animate = value =>
    Animated.timing(this.state.anim, {
      useNativeDriver: true,
      duration: 4000,
      toValue: value,
    }).start();

  render() {
    const { anim } = this.state;

    return (
      <Svg width={width} height={height}>
        <AnimatedG
          style={{
            transform: [
              {
                translateY: anim.interpolate({
                  inputRange: [0, 1],
                  outputRange: [0, 100],
                }),
              },
            ],
          }}
        >
          <Text>
            <TSpan>Test</TSpan>
          </Text>
        </AnimatedG>
      </Svg>
    );
  }
}

export default function App() {
  return <NativeAnimGTransform value={1} />;
}

@msand
Copy link
Collaborator

msand commented Oct 11, 2018

@adam-s Try something like this (with the latest commit from the master branch):

  callback(data) {
    this.ref.setNativeProps({ positionX: data.x });
  }

@xzilja
Copy link
Author

xzilja commented Oct 11, 2018

@msand I tried this with latest master and it is now working, thank you! :)

Few caveats that I think are worth mentioning, in your example you have useNativeDriver: true, this only works with rectwidth, for width it needs to be disabled or not present.

I will stick with using rectwidth for animations as they seem to be more performant, is this safe to do?

What I mentioned above, seems like a "hidden" knowledge at the moment i.e I would have never figure this out from the docs at the moment and am still not sure of difference between width and rectwidth and why one works with native driver and other doesn't? If you explain it and feel like it might deserve space in the documentation, I can help with PR 👍

Shall this issue be closed or shall I close it after fix is up on npm?

@wcandillon
Copy link
Contributor

wcandillon commented Oct 11, 2018

@msand The updates you just posted are super exciting. Looking forward to try it out with expo in the near future.

@iljadaderko Good to see you here 🙋🏼‍♂️

@msand
Copy link
Collaborator

msand commented Oct 11, 2018

The rectwith is needed if you want the native animation to work, the width and height properties have a naming collision with the same property name, so they have the element name prepended on the native side, and Animated needs the native name to be able to animate it with useNativeDriver set to true. Hopefully the naming collision could be avoided somehow, and the correct name would be sufficient.

@msand
Copy link
Collaborator

msand commented Oct 11, 2018

I would actually expect that we can get rid of the {use,mask,image,bb,pattern,rect}{width,height} attributes and just use width and height instead, as I've been able to override the transform
fb4e877#diff-50f7e7f8735df31a6ac08ae41511a2a6R262
fb4e877#diff-ba367d8c1ef85007a13efc389f1b0608R156
view property handlers in the view managers, it should be possible to do the same with them as well.

@xzilja
Copy link
Author

xzilja commented Oct 11, 2018

I would actually expect that we can get rid of the {use,mask,image,bb,pattern,rect}{width,height} attributes and just use width and height instead, as I've been able to override the transform
fb4e877#diff-50f7e7f8735df31a6ac08ae41511a2a6R262
fb4e877#diff-ba367d8c1ef85007a13efc389f1b0608R156
view property handlers in the view managers, it should be possible to do the same with them as well.

If thats achievable, it definitely sounds less confusing and easier to get going with, I wonder how many people will skip setting useNativeDriver to true however since in general rn doesn't support it for width, only transitions and opacity atm I think? If skipped, will it cause any errors?

@msand
Copy link
Collaborator

msand commented Oct 11, 2018

@iljadaderko I've simplified the handling in the way I thought should be possible: 445780c
Now you can skip rectwidth and simply use width instead, both with and without useNativeDriver:

import React from "react";
import { Dimensions, Animated } from "react-native";
import Svg, {
  LinearGradient,
  Defs,
  Rect,
  Stop,
  Text,
} from "react-native-svg";

const { width } = Dimensions.get("window");

const barWidth = width * 0.5;
const barHeight = barWidth * 0.5;
const AnimatedRect = Animated.createAnimatedComponent(Rect);
const AnimatedText = Animated.createAnimatedComponent(Text);

class HealthManaBar extends React.Component {
  state = {
    width: new Animated.Value(0),
  };

  componentDidMount() {
    const { currentValue, totalValue } = this.props;
    this.animate(currentValue, totalValue);
  }

  componentDidUpdate({ currentValue, totalValue }) {
    this.animate(currentValue, totalValue);
  }

  animate = (current, total) =>
    Animated.timing(this.state.width, {
      toValue: current / total,
      duration: 4000,
      useNativeDriver: true,
    }).start();

  render() {
    const { variant } = this.props;
    const { width } = this.state;

    return (
      <Svg width={barWidth} height={barHeight}>
        <Defs>
          <LinearGradient
            id={`HeathManaBar-gradient-${variant}`}
            x1="0"
            y1="0"
            x2="0"
            y2={barHeight}
          >
            <Stop
              offset="0"
              stopColor={variant === "HEALTH" ? "#EC561B" : "#00ACE1"}
              stopOpacity="1"
            />
            <Stop
              offset="0.5"
              stopColor={variant === "HEALTH" ? "#8D1B00" : "#003FAA"}
              stopOpacity="1"
            />
            <Stop
              offset="1"
              stopColor={variant === "HEALTH" ? "#9F3606" : "#007C97"}
              stopOpacity="1"
            />
          </LinearGradient>
        </Defs>
        <AnimatedRect
          x="0"
          y="0"
          rx="3"
          ry="3"
          width={width.interpolate({
            inputRange: [0, 1],
            outputRange: [0, barWidth],
          })}
          height={barHeight}
          fill={`url(#HeathManaBar-gradient-${variant})`}
        />
        <AnimatedText
          x={width.interpolate({
            inputRange: [0, 2],
            outputRange: [0, barWidth],
          })}
          y={width.interpolate({
            inputRange: [0, 2],
            outputRange: [0, barHeight],
          })}
        >
          Test
        </AnimatedText>
      </Svg>
    );
  }
}

export default function App() {
  return <HealthManaBar totalValue={100} currentValue={50} variant="HEALTH" />;
}

@msand
Copy link
Collaborator

msand commented Oct 11, 2018

The difference between useNativeDriver: false and true, is that when false: the animation is driven by javascript calling setNativeProps on the animated element; when true: the animation is serialised and sent over the bridge to be driven by the native animation driver events, without any further processing from js, which is much faster in essentially every case and doesn't get blocked by long running js computations. The main reason one would disable the native driver is if the properties one needs to animate aren't supported.

But, my commits from the past two days should be relatively good inspiration for how to get support for all the remaining properties. To get js driven (useNativeDriver: falsy) animation, the js property extraction logic needs to be run in the setNativeProps handler of the svg elements, and to get useNativeDriver: true support, the property setters in the native view managers need to change their input parameter type to Dynamic (java) / id (obj-c) and the appropriate transformation done from the values provided by the native driver to the types expected by the shadow nodes / native views.

These would be excellent first issues / PRs for anyone interested in contributing to wider animation support. ❤️

@adam-s
Copy link

adam-s commented Oct 11, 2018

I'm having luck using Victory Native primitives which wrap the react-native-svg elements. https://github.com/FormidableLabs/victory-native/tree/master/lib/components/victory-primitives

@msand
Copy link
Collaborator

msand commented Oct 11, 2018

Now with useNativeDriver animation support for all number accepting properties: 864d761

@xzilja
Copy link
Author

xzilja commented Oct 12, 2018

NOTE: This is a long response but it walks through issue and solution I found for another animation issue.

@msand seems to be working well with some cases I tested, I tried to use same approach in example below, essentially trying to replicate https://kimmobrunfeldt.github.io/progressbar.js yellow progress-bar circle from here, but with gradient colour. Approach we used for width does not work here, is this because strokeDasharray accepts array instead of number ?

import * as React from 'react';
import { Animated, StyleSheet, ViewProperties } from 'react-native';
import Svg, { Circle, Defs, LinearGradient, Stop } from 'react-native-svg';
import { deviceWidth } from '../services/Device';

const circleWidthHeight = deviceWidth * 0.2565;
const circleRadius = circleWidthHeight * 0.45;
const strokeThickness = deviceWidth * 0.007;
const circumference = circleRadius * 2 * Math.PI;

/**
 * Types
 */
export interface IProps {
  nextExp: number;
  currentExp: number;
  style?: ViewProperties['style'];
}

export interface IState {
  percentage: Animated.Value;
}

/**
 * Component
 */
class ExperienceCircle extends React.Component<IProps, IState> {
  state = {
    percentage: new Animated.Value(0)
  };

  componentDidMount() {
    const { currentExp, nextExp } = this.props;
    this.animate(currentExp, nextExp);
  }

  animate = (currentExp: number, nextExp: number) => {
    const percentage = currentExp / nextExp;
    Animated.timing(this.state.percentage, {
      toValue: percentage,
      useNativeDriver: true
    }).start();
  };

  render() {
    const { style } = this.props;
    const { percentage } = this.state;

    return (
      <Svg style={[styles.container, style]} width={circleWidthHeight} height={circleWidthHeight}>
        <Defs>
          <LinearGradient
            id="ExperienceCircle-gradient"
            x1="0"
            y1="0"
            x2="0"
            y2={circleWidthHeight * 2}
          >
            <Stop offset="0" stopColor="#DEF030" stopOpacity="1" />
            <Stop offset="0.5" stopColor="#71A417" stopOpacity="1" />
          </LinearGradient>
        </Defs>
        <Circle
          cx={circleWidthHeight / 2}
          cy={circleWidthHeight / 2}
          r={circleRadius}
          stroke="url(#ExperienceCircle-gradient)"
          strokeWidth={strokeThickness}
          fill="transparent"
          strokeDasharray={[
            percentage.interpolate({
              inputRange: [0, 1],
              outputRange: [0, circumference]
            }),
            circumference
          ]}
          strokeLinecap="round"
        />
      </Svg>
    );
  }
}

export default ExperienceCircle;

/**
 * Styles
 */
const styles = StyleSheet.create({
  container: {
    transform: [
      {
        rotate: '90deg'
      }
    ]
  }
});

As a workaround I tried to use setNativeProps approach below, but it throws following error

JSON value '300.120202' of type NSNumber cannot be converted to NSString

import * as React from 'react';
import { Animated, StyleSheet, ViewProperties } from 'react-native';
import Svg, { Circle, Defs, LinearGradient, Stop } from 'react-native-svg';
import { deviceWidth } from '../services/Device';

const circleWidthHeight = deviceWidth * 0.2565;
const circleRadius = circleWidthHeight * 0.45;
const strokeThickness = deviceWidth * 0.007;
const circumference = circleRadius * 2 * Math.PI;

/**
 * Types
 */
export interface IProps {
  nextExp: number;
  currentExp: number;
  style?: ViewProperties['style'];
}

export interface IState {
  percentage: Animated.Value;
}

/**
 * Component
 */
class ExperienceCircle extends React.Component<IProps, IState> {
  state = {
    percentage: new Animated.Value(0)
  };

  circleRef = React.createRef();

  componentDidMount() {
    this.state.percentage.addListener(percentage => {
      const dashLength = percentage.value * circumference;
      this.circleRef.current.setNativeProps({
        strokeDasharray: [dashLength, circumference]
      });
    });
    this.animate(this.props.currentExp, this.props.nextExp);
  }

  componentDidUpdate({ currentExp, nextExp }: IProps) {
    this.animate(currentExp, nextExp);
  }

  animate = (currentExp: number, nextExp: number) => {
    const percentage = currentExp / nextExp;
    Animated.timing(this.state.percentage, {
      toValue: percentage
    }).start();
  };

  render() {
    const { style } = this.props;

    return (
      <Svg style={[styles.container, style]} width={circleWidthHeight} height={circleWidthHeight}>
        <Defs>
          <LinearGradient
            id="ExperienceCircle-gradient"
            x1="0"
            y1="0"
            x2="0"
            y2={circleWidthHeight * 2}
          >
            <Stop offset="0" stopColor="#DEF030" stopOpacity="1" />
            <Stop offset="0.5" stopColor="#71A417" stopOpacity="1" />
          </LinearGradient>
        </Defs>
        <Circle
          ref={this.circleRef}
          cx={circleWidthHeight / 2}
          cy={circleWidthHeight / 2}
          r={circleRadius}
          stroke="url(#ExperienceCircle-gradient)"
          strokeWidth={strokeThickness}
          fill="transparent"
          strokeDasharray={[0, circumference]}
          strokeLinecap="round"
        />
      </Svg>
    );
  }
}

export default ExperienceCircle;

/**
 * Styles
 */
const styles = StyleSheet.create({
  container: {
    transform: [
      {
        rotate: '90deg'
      }
    ]
  }
});

After fiddling around with setNativeProps approach I got it working by adding toString here

 this.circleRef.current.setNativeProps({
     strokeDasharray: [dashLength.toString(), circumference.toString()]
 });

But I am really confused to why this works, as both of these seemed fine when numeric values are used straight on the <Circle />

@msand
Copy link
Collaborator

msand commented Oct 12, 2018

Yeah, this is expected, the stroke and fill extraction logic isn't run in setNativeProps (yet, perhaps a pr would be in order 😄), you can replicate the logic in the way you've done, or reuse parts of extractStroke (which is run in extractProps):
https://github.com/react-native-community/react-native-svg/blob/4e6ba9a786787672398dc32981ab81f2c3e8c187/elements/Circle.js#L24-L42

https://github.com/react-native-community/react-native-svg/blob/e7d0eb6df676d4f63f9eba7c0cf5ddd6c4c85fbe/lib/extract/extractProps.js#L42-L43

https://github.com/react-native-community/react-native-svg/blob/d0c0b048bfb9035cede8254436875ab5e57ea3f7/lib/extract/extractStroke.js#L30-L41

The native side expects an array of strings in this case:
https://github.com/react-native-community/react-native-svg/blob/864d7610d3aaea2b4e4f722f04ea52ddf292aae7/ios/ViewManagers/RNSVGRenderableManager.m#L39

https://github.com/react-native-community/react-native-svg/blob/fb4e877c2b23b4c52971c16d42c305d9f28a9eb5/ios/RNSVGRenderable.m#L114-L134

https://github.com/react-native-community/react-native-svg/blob/864d7610d3aaea2b4e4f722f04ea52ddf292aae7/android/src/main/java/com/horcrux/svg/RenderableViewManager.java#L1020-L1023

https://github.com/react-native-community/react-native-svg/blob/864d7610d3aaea2b4e4f722f04ea52ddf292aae7/android/src/main/java/com/horcrux/svg/RenderableShadowNode.java#L155-L167

I don't know if useNativeDriver allows animating an array of numbers this way, but at least it should work without it. Otherwise, you can use my fork of react-native which has support for string interpolation, it should work there at least.

@msand
Copy link
Collaborator

msand commented Oct 12, 2018

Reading the code now, I noticed that percentage values in strokeDashArray can't possibly be working correctly in iOS as it just takes the floatValue of the string.

@xzilja
Copy link
Author

xzilja commented Oct 12, 2018

@msand gotcha, I wasn't using % values, just had to stringily numbers in the array when I pass them to setNativeProps. It works fine that way 👍

Thank you for clarification. One final side question regarding this whole issue: Do you have approximate eta when changes on master might go up to npm?

@msand
Copy link
Collaborator

msand commented Oct 12, 2018

Great. Yeah, just thought I would mention the % issue as I noticed it's not spec conformant, more a note to self for later.

Well, I would love to have a bit more testing of it before releasing. But as things usually go, the only way to get significant amounts of testing is to make a new release, most people don't live on the bleeding edge of the master branch. I could probably merge some PRs and cut a new release today, to speed up the process. People can always downgrade to whatever version worked the best previously and open issues to report any problems.

@msand
Copy link
Collaborator

msand commented Oct 12, 2018

Published v7.1.0 now

@xzilja
Copy link
Author

xzilja commented Oct 12, 2018

So far works fine on my end, I will close this issue as its concern was addressed with this release. Thank you for amazing support!

@xzilja xzilja closed this as completed Oct 12, 2018
@msand
Copy link
Collaborator

msand commented Oct 12, 2018

You're welcome, happy we got it fixed 🎉
I've simplified the property handling in the javascript side now, as the view managers can handle both string and number types with the recent changes: 1e25870

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants