-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Comments
This seems to work fine at least: 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',
},
}); |
@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
I think with v7 we can use numbers here? At least I am able to use 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? |
UPDATE: Making Svg animated as well in my example didn't do the trick :/ |
Do you have useNativeDriver: false? |
@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 I also tried animating width of I'm not sure if it helps at all, but I added ref to my |
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" />;
} |
I'm having the same problem with props x, y on the Text, G, and TSpan components. |
It requires replicating these changes to those components and properties: ios: 7c012a9 android: @adam-s Could you attempt making the changes and open a pr? |
@adam-s I realized it requires some other changes in the js side as well: 2a43579 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" />;
} |
@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" />;
} |
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.)
|
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} />;
} |
@adam-s Try something like this (with the latest commit from the master branch): callback(data) {
this.ref.setNativeProps({ positionX: data.x });
} |
@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 I will stick with using 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 Shall this issue be closed or shall I close it after fix is up on npm? |
@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 🙋🏼♂️ |
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. |
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 |
If thats achievable, it definitely sounds less confusing and easier to get going with, I wonder how many people will skip setting |
@iljadaderko I've simplified the handling in the way I thought should be possible: 445780c 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" />;
} |
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. ❤️ |
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 |
Now with useNativeDriver animation support for all number accepting properties: 864d761 |
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 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
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 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 |
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): The native side expects an array of strings in this case: 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. |
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. |
@msand gotcha, I wasn't using 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? |
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. |
Published v7.1.0 now |
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! |
You're welcome, happy we got it fixed 🎉 |
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?[email protected]
[email protected]
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)
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 thatNSString 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?The text was updated successfully, but these errors were encountered: