Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Animating react-native-svg dash length of a <Circle />

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):

enter image description here

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

like image 758
Ilja Avatar asked Sep 10 '17 15:09

Ilja


People also ask

How do you animate a Circle in react-native?

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.

How do you animate a SVG Circle stroke?

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.


3 Answers

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):

result

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

  • At the top of the componet, we write,

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.

like image 97
Moji Izadmehr Avatar answered Sep 28 '22 04:09

Moji Izadmehr


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.

like image 30
Jason Gaare Avatar answered Sep 28 '22 06:09

Jason Gaare


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?

like image 27
Naderio Avatar answered Sep 28 '22 06:09

Naderio