Animation: respond to a reaction-native-svg dash with a length of <Circle / ">
Hi everyone, I'm trying to achieve an effect like this: https://kimmobrunfeldt.imtqy.com/progressbar.js (circle)
I managed to successfully compose some svg elements before using the setNativeProps approach, but this time it doesn’t work with the stroke length, below is the gif showing the current behavior (the circle changes from full to half full when it receives new details):
Essentially, I'm trying to animate this change instead of just clicking, below is the full source for this rectangular progress bar, the main idea is that it uses Circle and strokeDasharray to show circular progress, it gets currentExp and nextExp as values for a character’s experience, to calculate the percentage on the left before they reach the next lvl.
The component uses a fairly standard set of elements, in addition to several sizes / animations and color attributes from styles and the styled-components library for styling.
NOTE: the project imports this library from expo.io , but essentially react-native-svg
import React, { Component } from "react"; import PropTypes from "prop-types"; import styled from "styled-components/native"; import { Animated } from "react-native"; import { Svg } from "expo"; import { colour, dimension, animation } from "../Styles"; const { Circle, Defs, LinearGradient, Stop } = Svg; const SSvg = styled(Svg)` transform: rotate(90deg); margin-left: ${dimension.ExperienceCircleMarginLeft}; margin-top: ${dimension.ExperienceCircleMarginTop}; `; class ExperienceCircle extends Component { // -- prop validation ----------------------------------------------------- // static propTypes = { nextExp: PropTypes.number.isRequired, currentExp: PropTypes.number.isRequired }; // -- state --------------------------------------------------------------- // state = { percentage: new Animated.Value(0) }; // -- methods ------------------------------------------------------------- // componentDidMount() { this.state.percentage.addListener(percentage => { const circumference = dimension.ExperienceCircleRadius * 2 * Math.PI; const dashLength = percentage.value * circumference; this.circle.setNativeProps({ strokeDasharray: [dashLength, circumference] }); }); this._onAnimateExp(this.props.nextExp, this.props.currentExp); } componentWillReceiveProps({ nextExp, currentExp }) { this._onAnimateExp(currentExp, nextExp); } _onAnimateExp = (currentExp, nextExp) => { const percentage = currentExp / nextExp; Animated.timing(this.state.percentage, { toValue: percentage, duration: animation.duration.long, easing: animation.easeOut }).start(); }; // -- render -------------------------------------------------------------- // render() { const { ...props } = this.props; // const circumference = dimension.ExperienceCircleRadius * 2 * Math.PI; // const dashLength = this.state.percentage * circumference; return ( <SSvg width={dimension.ExperienceCircleWidthHeight} height={dimension.ExperienceCircleWidthHeight} {...props} > <Defs> <LinearGradient id="ExperienceCircle-gradient" x1="0" y1="0" x2="0" y2={dimension.ExperienceCircleWidthHeight * 2} > <Stop offset="0" stopColor={`rgb(${colour.lightGreen})`} stopOpacity="1" /> <Stop offset="0.5" stopColor={`rgb(${colour.green})`} stopOpacity="1" /> </LinearGradient> </Defs> <Circle ref={x => (this.circle = x)} cx={dimension.ExperienceCircleWidthHeight / 2} cy={dimension.ExperienceCircleWidthHeight / 2} r={dimension.ExperienceCircleRadius} stroke="url(#ExperienceCircle-gradient)" strokeWidth={dimension.ExperienceCircleThickness} fill="transparent" strokeDasharray={[0, 0]} strokeLinecap="round" /> </SSvg> ); } } export default ExperienceCircle; UPDATE: Expanded discussion and other examples (a similar approach working for different elements), available through the release sent to react-native-svg repo: <a3>
It's actually pretty simple when you know how SVG inputs work, one of the problems with native-SVG reactions (or SVG inputs, in general, is that it doesn't work with an angle), so when you want work on the circle, you need to transform the angle of the inputs that it takes, you can do this simply by writing a function such as (you definitely don't need to remember or fully understand how the conversion works, this is the standard):
function polarToCartesian(centerX, centerY, radius, angleInDegrees) { var angleInRadians = (angleInDegrees-90) * Math.PI / 180.0; return { x: centerX + (radius * Math.cos(angleInRadians)), y: centerY + (radius * Math.sin(angleInRadians)) }; } Then you add another function that can give you the details d in the correct format: function describeArc (x, y, radius, startAngle, endAngle) {
var start = polarToCartesian(x, y, radius, endAngle); var end = polarToCartesian(x, y, radius, startAngle); var largeArcFlag = endAngle - startAngle <= 180 ? "0" : "1"; var d = [ "M", start.x, start.y, "A", radius, radius, 0, largeArcFlag, 0, end.x, end.y ].join(" "); return d; } Now this is great, you have a function (describeArc) that gives you the ideal parameter needed to describe your path (circular arc): so you can define PATH as:
<AnimatedPath d={_d} stroke="red" strokeWidth={5} fill="none"/> for example, if you need a circular arc with a radius R between 45 degrees to 90 degrees, simply define _d as:
_d = describeArc(R, R, R, 45, 90); Now that we know everything about how SVG PATH works, we can implement a native animation reaction and define an animated state, such as progress :
import React, {Component} from 'react'; import {View, Animated, Easing} from 'react-native'; import Svg, {Circle, Path} from 'react-native-svg'; AnimatedPath = Animated.createAnimatedComponent(Path); class App extends Component { constructor() { super(); this.state = { progress: new Animated.Value(0), } } componentDidMount(){ Animated.timing(this.state.progress,{ toValue:1, duration:1000, }).start() } render() { function polarToCartesian(centerX, centerY, radius, angleInDegrees) { var angleInRadians = (angleInDegrees-90) * Math.PI / 180.0; return { x: centerX + (radius * Math.cos(angleInRadians)), y: centerY + (radius * Math.sin(angleInRadians)) }; } function describeArc(x, y, radius, startAngle, endAngle){ var start = polarToCartesian(x, y, radius, endAngle); var end = polarToCartesian(x, y, radius, startAngle); var largeArcFlag = endAngle - startAngle <= 180 ? "0" : "1"; var d = [ "M", start.x, start.y, "A", radius, radius, 0, largeArcFlag, 0, end.x, end.y ].join(" "); return d; } let R = 160; let dRange = []; let iRange = []; let steps = 359; for (var i = 0; i<steps; i++){ dRange.push(describeArc(160, 160, 160, 0, i)); iRange.push(i/(steps-1)); } var _d = this.state.progress.interpolate({ inputRange: iRange, outputRange: dRange }) return ( <Svg style={{flex: 1}}> <Circle cx={R} cy={R} r={R} stroke="green" strokeWidth="2.5" fill="green" /> {/* X0 Y0 X1 Y1*/} <AnimatedPath d={_d} stroke="red" strokeWidth={5} fill="none"/> </Svg> ); } } export default App; This simple component will work the way you want.
- At the top of the component we write
AnimatedPath = Animated.createAnimatedComponent(Path);
because the PATH , which is imported from response-native-svg, is not a native component of native-native, and we turn it into an animated one by this.
at
constructorwe defined progress as an animated state that should change during the animation.at
componentDidMountthe animation process starts.at the beginning of the
rendermethod, two functions are declared that are necessary to determine the SVGdparameters (polarToCartesiananddescribeArc).then the native
interpolatereaction is used onthis.state.progressto interpolate the change inthis.state.progressfrom 0 to 1, on changing the d parameter. However, there are two points:1- the change between two arcs with different lengths is not linear, so linear interpolation from an angle of 0 to 360 does not work as you would like, as a result, it is better to determine the animation at different stages of n degrees (I used 1 degree, u can increase or reduce it if necessary.).
A 2-arc cannot extend to 360 degrees (because it is equivalent to 0), so it’s better to finish the animation to a degree close to but not equal to 360 (for example, 359.9).
At the end of the return section, the user interface is described.
Another absolute large library for svg animation is https://maxwellito.imtqy.com/vivus/ It is standalone without dependencies and easy to use.
Perhaps this fits your needs?
If you are not tied to the svg library, I think you can check out this library: https://github.com/bgryszko/react-native-circular-progress , this could be a much easier way to achieve what you are looking for.

