software-mansion / react-native-svg

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

use with animation #180

Closed anhuiliujun closed 5 years ago

anhuiliujun commented 7 years ago

Can it uses with react-native's Animated

AlbertBrand commented 7 years ago

Yes, it can. Probably you want to create Animated variants of each component (with Animated.createAnimatedComponent) to useAnimated.Value` objects as properties.

grubstarstar commented 7 years ago

I'm struggling to use Animated to animate the 'd' property on a Path component. It looks like this is because Animated.Values can only be numerical. So I reverted to trying to use setNativeProps on the Path component to set 'd', but an error is thrown.

Can you tell me if setNativeProps is supported on Paths? Thanks!

AlbertBrand commented 7 years ago

I think d property is atm impossible to do with Animated as you only can interpolate from number to string. I've not tried the setNativeProps route, maybe you can show your code? I could try and figure out what's happening in native. Btw on Android or iOS?

grubstarstar commented 7 years ago

Thanks for your response. I tried a few approaches this morning which I'll share with you. There approaches are as follows:

  1. using setState
  2. attempting to use setNativeProps
  3. using interpolation

using setState works fine, but I just thought there would be a more performant way of doing it that having to re-render the tree each frame, which is why I attempted using setNativeProps. The error message I get for the second and third approach are the same, i'm assuming this is because Animated.Value just uses setNativeProps internally. This is the error message. It looks like it's on the native side (iOS) but I haven't investigated it any further.

screen shot 2016-12-16 at 11 05 46 am

Here are the code snippets I used for each:

using setState

export default class testing extends Component {

   constructor(props) {
      super(props)
      this._origin = { x: 100, y: 100 }
      this._radius = 50
      this.state = {
         arcEndX: Math.sin(0) * this._radius,
         arcEndY: Math.cos(0) * this._radius - this._radius,
         largeArcFlag: Math.sin(0) >= 0 ? 0 : 1
      }
      this.setArcEndFromRadians = this.setArcEndFromRadians.bind(this)
   }

   setArcEndFromRadians(radians) {
      this.setState({
         arcEndX: Math.sin(radians) * this._radius,
         arcEndY: Math.cos(radians) * this._radius - this._radius,
         largeArcFlag: Math.sin(radians) >= 0 ? 0 : 1
      })
   }

   componentDidMount() {
      let radians = 0
      let timer = setInterval(() => {
         radians += 0.02
         this.setArcEndFromRadians(radians)
      }, 16)
   }

   render() {
      return (
         <View>
            <Svg
               height="200"
               width="200">
               <Path
                  d={ `M ${this._origin.x},${this._origin.y} l 0,50 a 50,50 0 ${this.state.largeArcFlag} 0 ${this.state.arcEndX},${this.state.arcEndY} z` }/>
            </Svg>
         </View>
      )
   }

}

using setNativeProps (for better performance, error thrown)

export default class testing extends Component {

   constructor(props) {
      super(props)
      this._origin = { x: 100, y: 100 }
      this._radius = 50
      this._arc = {
         arcEndX: Math.sin(0) * this._radius,
         arcEndY: Math.cos(0) * this._radius - this._radius,
         largeArcFlag: Math.sin(0) >= 0 ? 0 : 1
      }
      this.setArcEndFromRadians = this.setArcEndFromRadians.bind(this)
   }

   setArcEndFromRadians(radians) {
      let arcEndX = Math.sin(radians) * this._radius
      let arcEndY = Math.cos(radians) * this._radius - this._radius
      let largeArcFlag = Math.sin(radians) >= 0 ? 0 : 1
      this._thePath.setNativeProps({
         d: `M ${this._origin.x} ${this._origin.y} l 0 50 a 50,50 0 ${largeArcFlag} 0 ${arcEndX} ${arcEndY} z`
      })
   }

   componentDidMount() {
      let radians = 0
      let timer = setInterval(() => {
         radians += 0.02
         this.setArcEndFromRadians(radians)
      }, 16)
   }

   render() {
      return (
         <View>
            <Svg
               height="200"
               width="200">
               <Path
                  ref={ ref => this._thePath = ref }
                  d={ `M ${this._origin.x},${this._origin.y} l 0,50 a 50,50 0 ${this._arc.largeArcFlag} 0 ${this._arc.arcEndX},${this._arc.arcEndY} z` }/>
            </Svg>
         </View>
      )
   }

}

using Animated interpolation (simplified to just drawing a line, rather than a circle, error thrown)

let AnimatedPath = Animated.createAnimatedComponent(Path)
export default class testing extends Component {

   constructor(props) {
      super(props)
      this._origin = { x: 100, y: 100 }
      this._radians = new Animated.Value(0)
      this._arcX = this._radians.interpolate({
         inputRange: [
            0,
            100
         ],
         outputRange: [
            `M ${this._origin.x},${this._origin.y} l 0,0`,
            `M ${this._origin.x},${this._origin.y} l 100,100`
         ]
      })
   }

   componentDidMount() {
      Animated.spring(this._radians, {
         toValue: Math.PI,
         friction: 5,
         tension: 135
      }).start()
   }

   render() {
      return (
         <View>
            <Svg
               height="200"
               width="200">
               <AnimatedPath
                  d={ this._arcX }/>
            </Svg>
         </View>
      )
   }

}
Nimmimarco commented 7 years ago

Components has method setNativeProps but he doesnt work. Is there any other normal ways animate svg's part, other then just to wrap each in Svg?

Lyonsclay commented 7 years ago

I was able to animate an Svg object by animating a react-native View component wrapped around it.

import React, { Component } from 'react';
import {
  View,
  Animated,
  Easing
} from 'react-native';
import Svg, {
  Line,
  G,
  Text
} from 'react-native-svg';

class MovingHand extends Component {
  constructor(props){
    super(props)

    const { remainder, duration } = props.timer
    const start =  duration - remainder
    this.state = {
      wind: new Animated.Value(start),
      duration,
      start
    }
  }

  componentDidMount() {
    Animated.timing(
      this.state.wind,
      {
        toValue: 1,
        duration: this.state.start * 1000,
        easing: Easing.none,
      }
    ).start()
  }

  render() {
    const {
      width,
      height,
      radius,
      strokeWidth,
    } = this.props;
    const { start, duration } = this.state 
    const motionStyle = {
      transform: [{
        rotate: this.state.wind.interpolate({
          inputRange: [0, 1],
          outputRange: ['0deg', '360deg']
        })
      }]
    }

    return (
      <Animated.View style={motionStyle}>
        <Svg width={width} height={height}>
          <Line
            x1={radius}
            y1={0.20 * radius}
            x2={radius}
            y2={radius}
            stroke='brown'
            strokeWidth={2 * strokeWidth}
            strokeLinecap='round'
          />
        </Svg>
      </Animated.View>
    )
  }
}

export default MovingHand;
ameliabradley commented 7 years ago

Getting a very similar issue when attempting to animate using color values

screen shot 2017-02-26 at 12 33 33 am

joshuapinter commented 7 years ago

At the risk of posting to more than one animation issue, is there any way to animate an SVG element without wrapping it in a Animated View?

I'm trying to adjust the radius of a circle without using scale transform in the styles, because I later want to adjust the start and end points of a SVG Line.

Here's what I'm running into:

// var AnimatedCircle = Animated.createAnimatedComponent(Circle); <-- Done before.

<AnimatedCircle cx="250" cy="250" r={this.state.circleRadius} fill="black" />

With the following animation on mount:

// this.state.circleRadius = new Animated.Value(50) <-- Starting value

Animated.spring(
  this.state.circleRadius,
  { toValue: 100, friction: 3 }
).start();

And getting the following error:

screenshot 2017-03-03 11 00 44

anhtuank7c commented 7 years ago

@joshuapinter Try:

<AnimatedCircle 
    cx="250" 
    cy="250" 
    r={`${this.state.circleRadius}`} 
    fill="black" 
/>
joshuapinter commented 7 years ago

@anhtuank7c Thanks for the suggestion but now I get this error message:

Invalid float: "[object Object]"

screenshot 2017-03-10 08 29 29

udfalkso commented 7 years ago

@grubstarstar @leebradley I'm having exactly the same issue. Did you ever find a workable solution? Thanks!

I opened this before finding this thread: https://github.com/react-native-community/react-native-svg/issues/326

grubstarstar commented 7 years ago

@udfalkso I found that simply using setState as per my first example above was sufficiently performant in the end for my needs. @Lyonsclay solution of animating the wrapping View is good if you can accomplish what you need using that but I couldn't in my case. I found that once I had my solution built in release mode the performance was more acceptable.

It would be great to be able to use with Animated though, especially if it could hand over the animation details to the native thread using "useNative" https://facebook.github.io/react-native/blog/2017/02/14/using-native-driver-for-animated.html it seems like this is where animation belongs.

udfalkso commented 7 years ago

Thanks @grubstarstar. I'm already doing the wrapping View trick for opacity changes, but I can't do that for fill color.

grubstarstar commented 7 years ago

@udfalkso nah, true. Is setState too slow? I managed a pretty smooth radial animation just using that.

joshuapinter commented 7 years ago

@grubstarstar setState's performance really depends on what else you're doing in the component's methods, like render. For simple components, setState may be sufficient but it quickly loses value on more complex components.

The advantage of using Animated is that it completes the animation on the UI thread so is more or less unaffected by what else your component is trying to do.

udfalkso commented 7 years ago

@grubstarstar Yes unfortunately in my case I'm doing this for many paths at the same time (20+) and things grind to a halt.

udfalkso commented 7 years ago

@grubstarstar @joshuapinter I managed to get it working with some fairly small changes.

Check out the PR. I hope it works for you guys too: https://github.com/react-native-community/react-native-svg/pull/328

The only gotcha is that you still have to use this.myAnim.__getAnimatedValue() when sending the value into setNativeProps

animate = () => {
    if (this._path && this.fillColor) {
      this._path.setNativeProps({
        fill: this.fillColorInterpolation.__getAnimatedValue(),
      })
    }
    requestAnimationFrame(this.animate.bind(this))
  }
grubstarstar commented 7 years ago

@udfalkso good work! I'll take a look at that...

joshuapinter commented 7 years ago

Hi guys,

I just wanted to follow up from my comment.

Found a workable solution using addListener and using setNativeProps. A little messy but works none-the-less and is quite performant.

Here's a simplified version of the solution:

constructor(props) {
  super(props);

  this.state = { circleRadius: new Animated.Value(50) };

  this.state.circleRadius.addListener( (circleRadius) => {
    this._myCircle.setNativeProps({ r: circleRadius.value.toString() });
  });

  setTimeout( () => {
    Animated.spring( this.state.circleRadius, { toValue: 100, friction: 3 } ).start();
  }, 2000)
}

render() {
  return(
    <Svg height="400" width="400">
      <AnimatedCircle ref={ ref => this._myCircle = ref } cx="250" cy="250" r="50" fill="black" />
    </Svg>
  )
}

And the resulting animation:

circle animation with setnativeprops

And this is being rendered on a very complex component where using setState isn't fluid at all.

dk0r commented 7 years ago

@joshuapinter Thanks. I implemented your animated circle and tried to extend the example to a Polygon (in an attempt to animate a point in the polygon) but received the following error:

undefined is not a function (evaluating '_this3.root.getNativeElement()')

Here's my source for both the broken polygon and the working circle: https://gist.github.com/dk0r/76761b6cc5a3069b9443fabb81801a55 Error

ethantran commented 7 years ago

If you guys need examples on how to get most of the components animated, check my comment at #55

anhtuank7c commented 7 years ago

@joshuapinter Your solution working good while update r, i tried to update rotate but nothing changed :(

kuongknight commented 6 years ago

@dk0r I found same error with Polyline :(

joshuapinter commented 6 years ago

@anhtuank7c There are only certain attributes that are exposed to setNativeProps. I'm not sure what they are but if anybody finds a good list of them, please post it here!

zachgibson commented 6 years ago

Do y’all think we’ll ever be able to offload animations on the native side via useNativeDriver?

msand commented 6 years ago

Its fully possible, just have to integrate with the driver and the declarative animation passing ;) You wanna take a shot at it?

msand commented 6 years ago

@joshuapinter For the native props you can check the view managers https://github.com/react-native-community/react-native-svg/tree/master/ios/ViewManagers

zachgibson commented 6 years ago

@msand I’d like to possibly take a stab at it. :) I’m not very familiar with how this might work though. Would every SVG prop need to be implemented for natively driving animation?

msand commented 6 years ago

@zachgibson I don't really know. But, I think somehow on the native side we need to allow animated values as props, and then register onchange listeners to actually set the values on the shadow nodes and invalidate up the tree to queue a re-rendering of the bitmap. It might be a really minor change, just have to figure out how to glue the listener and the animated/shadow nodes. @vjeux Could you mentor here a bit, would be much appreciated!

vjeux commented 6 years ago

I’m sorry but I haven’t been working on react native for 1.5 years now... i won’t be able to spend time on this :(

msand commented 6 years ago

Alright, no probs. I managed to animate opacity of a Svg Rect with the native driver in iOS, by removing this one method 😃 https://github.com/react-native-community/react-native-svg/blob/152e839126e66a708a9492d203ef7fb4302e1030/ios/ViewManagers/RNSVGNodeManager.m#L27-L30

joshjhargreaves commented 6 years ago

@msand I had just done the same actually, but had been trying to animate the radius of SVG circle (getting RCTConvert errors still)!

It looks like viewNameForRectTag will come back nilhere, unless the view gets added to the map of shadowViews here if you remove that line.

RCTShadowView seems to have a tonne of yoga stuff, so still not sure what makes the most sense to do.

msand commented 6 years ago

Yeah I was having some issues with animating the props represented as NSString, haven't figured out how to deal with that yet. But CGFloat based ones seem to work with just removing that one method to get the shadow nodes registered, trying to figure out how to get it to skip the YGValue conversion now.

joshjhargreaves commented 6 years ago

ah good observation! I'm stuck on the same 🤔

msand commented 6 years ago

I think any property name collisions with RCTShadowView need to be pre-fixed with rnsvg or something similar. Or now that I think about it, the removed method probably needs to exist, and give a shadow node corresponding to the element.

joshjhargreaves commented 6 years ago

@msand this looks pretty suspect: https://github.com/react-native-community/react-native-svg/blob/master/elements/Circle.js#L35, for example.

joshjhargreaves commented 6 years ago

It might not actually have any affect actually, as createAnimatedComponent does some HOC magic.

joshjhargreaves commented 6 years ago

Ah so yes, that was part of the problem actually! Just as a proof of concept I had changed the exported view property type of r on RNSVGCircle to NSNumber, and then converted the number value to a string inside the setter of r. But the toString in Circle.js, would've meant a string would've still been passed to the native side for the first render of the circle, thus failing the props validation.

But with that, animating the radius works with useNativeDriver!

I had my suspicions before, but I'm pretty sure this is not anything to do with property name collisions before with RCTShadowView, as I had hardcoded the 'viewName' to test this.

msand commented 6 years ago

Most of the properties are strings, because they support units and they depend on e.g. the font-size and clip bounds so need to be passed as such to the native side for the css resolution logic. E.g. Rect has a name collision for width and height with RCTShadowView causing it to use the YGValue converter.

joshjhargreaves commented 6 years ago

It looks like the string property aspect is a bit of a pain point! I'll have a look some of the string properties that support units in the documentation as I'm not too familiar with that myself. Having a separate prop for each unit like so fontSizePx and fontSizeEm would allow us to get around this issue by the looks of it for example, and use numbers for these props.

However it might actually be useful to have a Animated.convertToString native method where you could do something like this animatedValue.convertToString('${value}px'), and you'd pass that to your fontSize prop. I'm not sure if this would be useful for anything else outside of react-native svg? 🤔

msand commented 6 years ago

I've managed to hack together a proof of concept for android as well, but I'm really hoping it's the wrong approach and that there is some better way of getting it to work, using this approach it would have to create a View/ViewGroup for each node, at least it doesn't have to be attached to the layout tree, but its silly to have it, when all I use it for is to get the id / shadow node corresponding to it. Also the queueing of the rendering might not be as efficient as it should be. Any feedback would be much appreciated! https://github.com/msand/react-native-svg/commit/fbd65912512aa74c00d13d32a26c1f7b3e538b46 (Edit force-pushed some missing files)

msand commented 6 years ago

@joshyhargreaves I have a PR to react-native for string interpolation on iOS now 😄https://github.com/facebook/react-native/pull/18187 Waiting for some feedback, and if good will port it to Android as well. Allowing us to animate all of the string props with unit support. Added the missing ReactProps to Android as well https://github.com/msand/react-native-svg/commit/a3c9aa287257eb6ef8a6853dded0a490b0b1830c

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

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

function getInitialState() {
  const anim = new Animated.Value(0);
  const fillOpacity = anim.interpolate({
    inputRange: [0, 1],
    outputRange: [0, 1],
  });
  const offset = fillOpacity.interpolate({
    inputRange: [0, 1],
    outputRange: [0, 10],
  });
  const strokeOpacity = offset.interpolate({
    inputRange: [0, 5],
    outputRange: [0, 1],
    extrapolateRight: 'clamp',
  });
  const strokeWidth = strokeOpacity.interpolate({
    inputRange: [0, 1],
    outputRange: ['0', '5'],
  });
  return { anim, fillOpacity, offset, strokeOpacity, strokeWidth };
}

export default class App extends Component {
  state = getInitialState();

  componentDidMount() {
    const { anim } = this.state;
    Animated.timing(anim, {
      toValue: 1,
      duration: 3000,
      useNativeDriver: true,
    }).start();
  }

  render() {
    const { fillOpacity, offset, strokeOpacity, strokeWidth } = this.state;
    return (
      <View style={styles.container}>
        <Svg width={width} height={height} viewBox="0 0 100 100">
          <AnimatedRect
            x="5"
            y="5"
            width="90"
            height="90"
            stroke="blue"
            fill="green"
            strokeDasharray="1 1"
            strokeWidth={strokeWidth}
            strokeDashoffset={offset}
            strokeOpacity={strokeOpacity}
            fillOpacity={fillOpacity}
          />
        </Svg>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    backgroundColor: '#ecf0f1',
  },
});
msand commented 6 years ago

Now with android support for native string interpolation as well: https://github.com/msand/react-native/commit/d3d6e669fcf5e53c7b8c8cb33dbe8e81f587cef3

oriharel commented 6 years ago

@msand any update about the PR?

oriharel commented 6 years ago

@msand also, can you show an example with 'd' attribute?

designingSparks commented 6 years ago

@msand Your answer from 5 March was very helpful. However, it doesn't seem to work when animating rotation properties. (See this issue). Do you have any idea on how to solve this?

msand commented 6 years ago

v7.0.0 has been released with support for useNativeDriver

msand commented 6 years ago

@designingSparks You probably need to use setNativeProps with a matrix for the transform for now, you might want to ask @msageryd

msageryd commented 6 years ago

@designingSparks I didn't actually cope with the matrix algebra needed, so I'm using a library for the calculations.

https://gitlab.com/epistemex/transformation-matrix-js

This library let's you chain your transforms and gives a matrix back to you. Get hold of a ref to your svg component (I'm moving around G-elements). Use the components of the returned matrix as input to setNativeProps like this:

const matrix = new Matrix()
  .translate(x, y)
  .rotateDeg(45)
  .scaleU(0.5);

this.svgGroupRef.setNativeProps({
  matrix: [matrix.a, matrix.b, matrix.c, matrix.d, matrix.e, matrix.f], //[scaleX, 0, 0, scaleY, x, y],
});

The comment "[scaleX, .." is only roughly what the components means. It serves as a reminder of what goes where, but don't use it as the complete truth of the components.

Edit: I managed to find the original discussion, which helped me with this: https://github.com/react-native-community/react-native-svg/issues/556#issuecomment-354099452

msand commented 6 years ago

@oriharel For animation of the Path d attribute check here: https://github.com/facebook/react-native/pull/18187#issuecomment-414157937 Although, be aware that the path parsing on android is quite inefficient at the moment, and one of the biggest consumers of cpu when animating the path data. At least from what I could see when profiling that example in android studio.