Hey everyone I'm trying to achieve effect similar to: https://kimmobrunfeldt.github.io/progressbar.js (circle one)
I was able to successfully animate some svg elements before using setNativeProps
approach, but it is failing for me this time with dash length, below is a gif demonstrating current behaviour (circle is change from full to semi full when it receives new props):
Essentially I am trying to animate this change instead of it just flicking in, below is full source for this rectangular progress bar, basic idea is that is uses Circle
and strokeDasharray
in order to show circular progress, it receives currentExp
and nextExp
as values for characters experience in order to calculate percentage left before they reach next lvl.
Component uses pretty standard set of elements, besides few dimension / animation and colour props from stylesheed and styled-components
library for styling.
NOTE: project is importing this library from expo.io but it's 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: Extended discussion and more examples (similar approach working for different elements) available via issue posted to react-native-svg
repo: https://github.com/react-native-community/react-native-svg/issues/451
import React, {Component} from 'react'; import {View, Text, Animated, StyleSheet, Easing} from 'react-native'; export default class Circle extends Component { constructor() { super(); this. animated = new Animated. Value(0); var inputRange = [0, 1]; var outputRange = ['0deg', '360deg']; this. rotate = this.
You can animate strokes by using a stroke-dasharray that has the length of your circle (2 * PI * r) and a dash offset of equal length. Play around with the animation values of your dash length and offset to create different effects. Here's an example of how to do so.
It is actually quite simple when you know how SVG inputs work, one of the problems with react-native-SVG (or SVG inputs, in general, is that it doesn't work with angle), so when you want to work on a circle you need to transform angle to the inputs which it takes, this can be done, by simply writing a function such as (you necessarily don't need to memorize or totally understand how transformation 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 which can give you the d props in the right 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 it is great, you have the function (describeArc) which gives you the perfect parameter you need to describe your path (an arc of a circle):
so you can define the PATH
as:
<AnimatedPath d={_d} stroke="red" strokeWidth={5} fill="none"/>
for example, if you need an arc of a circle of 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 react native animation, 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 as you want
AnimatedPath = Animated.createAnimatedComponent(Path);
because Path
which is imported from react-native-svg is not native react-native component and we turn it to animated by this.
at constructor
we defined progress as the animated state which should change during animation.
at componentDidMount
the animation process is started.
at the beginning of render
method, the two functions needed to define SVG d
parameters are declared (polarToCartesian
and describeArc
).
then react-native interpolate
is used on this.state.progress
to interpolate the change in this.state.progress
from 0 to 1, into change in d parameter. However, there are two points here that you should bear in mind:
1- the change between two arcs with different lengths is not linear, so linear interpolation from angle 0 to 360 does not work as you would like, as a result, it is better to define the animation in different steps of n degrees (i used 1 degrees, u can increase or decrease it if needed.).
2- arc cannot continue up to 360 degrees (because it is equivalent to 0), so it is better to finish animation at a degree close to but not equal to 360 (such as 359.9)
at the end of the return section, the UI is described.
If you aren't tied to the svg library, I think you could checkout this library: https://github.com/bgryszko/react-native-circular-progress, it might be a much simpler way to achieve what your looking for.
Another absolute great library for animating svg's is https://maxwellito.github.io/vivus/ This is standalone without dependencies and easy to use.
Maybe this fits your needs?
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With