An exploration of border animation using SVG in React-Native

Rotate around the border? SVG!

It is well known that React native is a popular technology stack for front-end ape developers. Due to its efficient cross-platform capability and low-cost technology migration (compared to react developers), it is very popular among front-end ape developers. You only need a little native development knowledge and hundreds of millions of react development knowledge to play easily. So the first thing that comes to mind when you see this requirement is SVG!

Why not use the React art library? React-native has an art library to use, but the API is different from that of SVG. The library also has some unimplemented apis, such as the Shape fill attribute on Android:


  /**
   * Sets up {@link #mPaint} according to the props set on a shadow view. Returns {@code true}
   * if the fill should be drawn, {@code false} if not.
   */
  protected boolean setupFillPaint(Paint paint, float opacity) {
    if(mBrushData ! =null && mBrushData.length > 0) {
      paint.reset();
      paint.setFlags(Paint.ANTI_ALIAS_FLAG);
      paint.setStyle(Paint.Style.FILL);
      int colorType = (int) mBrushData[0];
      switch (colorType) {
        case COLOR_TYPE_SOLID_COLOR:
          paint.setARGB(
              (int) (mBrushData.length > 4 ? mBrushData[4] * opacity * 255 : opacity * 255),
              (int) (mBrushData[1] * 255),
              (int) (mBrushData[2] * 255),
              (int) (mBrushData[3] * 255));
          break;
        case COLOR_TYPE_LINEAR_GRADIENT:
          // For mBrushData format refer to LinearGradient and insertColorStopsIntoArray functions in ReactNativeART.js
          if (mBrushData.length < 5) {
            FLog.w(ReactConstants.TAG,
              "[ARTShapeShadowNode setupFillPaint] expects 5 elements, received "
              + mBrushData.length);
            return false;
          }
          float gradientStartX = mBrushData[1] * mScale;
          float gradientStartY = mBrushData[2] * mScale;
          float gradientEndX = mBrushData[3] * mScale;
          float gradientEndY = mBrushData[4] * mScale;
          int stops = (mBrushData.length - 5) / 5;
          int[] colors = null;
          float[] positions = null;
          if (stops > 0) {
            colors = new int[stops];
            positions = new float[stops];
            for (int i=0; i<stops; i++) {
              positions[i] = mBrushData[5 + 4*stops + i];
              int r = (int) (255 * mBrushData[5 + 4*i + 0]);
              int g = (int) (255 * mBrushData[5 + 4*i + 1]);
              int b = (int) (255 * mBrushData[5 + 4*i + 2]);
              int a = (int) (255 * mBrushData[5 + 4*i + 3]);
              colors[i] = Color.argb(a, r, g, b);
            }
          }
          paint.setShader(
            new LinearGradient(
              gradientStartX, gradientStartY,
              gradientEndX, gradientEndY,
              colors, positions,
              Shader.TileMode.CLAMP
            )
          );
          break;
        case COLOR_TYPE_RADIAL_GRADIENT:
          // TODO(6352048): Support radial gradient etc.
        case COLOR_TYPE_PATTERN:
          // TODO(6352048): Support patterns etc.
        default:
          FLog.w(ReactConstants.TAG, "ART: Color type " + colorType + " not supported!");
      }
      if (mShadowOpacity > 0) {
        paint.setShadowLayer(mShadowRadius, mShadowOffsetX, mShadowOffsetY, mShadowColor);
      }
      return true;
    }
    return false;
  }
Copy the code

You don’t implement patterns to fill the background, which is filled with images. Would you dare to use TODO if you saw so many? The React-native-SVG library is compatible with many of the tag attributes of Web SVG, making it more comfortable to use without the added learning burden. Lazy people prefer this library.

So where to start? First look at how SVG draws borders!

Since it is compatible with web tag properties, take a look at CSS to create a moving tag. Open the SVG section of the W3C and turn to the SVG Rect section. The rect attributes include:

  • X: Draws the X-axis coordinates of the starting point
  • Y: Draws the Y-axis coordinates of the starting point
  • Rx, RY: The rx and RY attributes round the rectangle
  • Width, height: width and height
  • Style:
    • The fill property defines the fill color of the rectangle (RGB value, color name, or hexadecimal value)
    • The stroke-width property defines the width of the rectangle border
    • The stroke property defines the color of the rectangle border
    • Stroke-dasharray document not written, used to draw dotted lines [length of dotted lines, length of interval between dotted lines]
    • The stroke-Dashoffset document is still unwritten and is used to set the offset of the starting point of the dashed line

Stroke-width defines the width of the border, stroke-dasharray defines the length and spacing of the border, and stroke-dashoffset defines the offset of the starting point so that the border can be drawn.

But how do we get him to spin? Animation!

So how do we get a fixed length border to turn? Here’s another small detail. We use the stroke-Dashoffset property to add a linearly varying offset to the border so that it turns.

It’s easy to animate SVG tags in CSS. You just need to define an animation for stroke-Dashoffset. How can you animate React-Native without animation tags? The answer is to use the react – native to provide Animated method, because the Rect are three components, so you need to use Animated. CreateAnimatedComponent (the Rect) package to use.

Now that we have a general idea of what we need to do, we need to determine the length and interval of the strokeDasharray dotted line, and we need to animate the animation by calculating the offset using the animation algorithm. The offset of the rectangle should not be set arbitrarily, so we need to calculate the circumference of the rectangle.

init = () = > {
    // Calculate the perimeter
    const { width, height } = this.state;
    const { borderRadius } = this.props;
    let rx = borderRadius;
    let ry = borderRadius;
    const maxBorderRadius = width < height ? width : height;

    if (borderRadius <= 0) {
        rx = 0;
        ry = 0;
    }

    if (borderRadius >= maxBorderRadius / 2) {
        rx = maxBorderRadius / 2;
        ry = maxBorderRadius / 2;
    }

    this.rx = rx;
    this.ry = ry;

    const borderRadiusLength = Math.PI * rx * 2; // The circumference of the rounded corner
    const lineLength = 2 * width + 2 * height - 8 * rx;

    this.setState({
        allLength: borderRadiusLength + lineLength,
    });
};
Copy the code

Then, naturally, we wrote the layout code like this:

<Svg
    viewBox={` 0 0${containerWidth} ${containerHeight}`}
    width={containerWidth}
    height={containerHeight}
    style={{
        position: 'absolute'.zIndex: 9999.elevation: 1,}} ><AnimatedRect
        ref={ref= > (this._myRect = ref)}
        x={borderWith}
        y={borderWith}
        rx={this.rx}
        ry={this.ry}
        width={width}
        height={height}
        fill="none"
        stroke={color}
        strokeWidth={borderWith}
        strokeDasharray={[
            borderLength,
            allLength - borderLength,
        ]}
    />
</Svg>
Copy the code

Explain why the strokeDasharray dash interval should be set to the perimeter minus the border length, because we need to loop the animation, when a duration runs through a perimeter offset, it needs to return to the initial value of 0 and continue to run the next offset, In this way, our interval plus the border length should be exactly equal to a perimeter, or we can continue to run the next multiple of offset. There are many methods, which can be understood naturally. If you are interested in viewBox and fill, you can refer to the document yourself.

Finally, look at how the animation API is called:

...constructor(props) {
    super(props);
    this.state = {
        rectDasharray: new Animated.Value(0),};// Listen for value changes and set
    this.state.rectDasharray.addListener(rectDasharray= > {
        this._myRect &&
            this._myRect.setNativeProps({
                strokeDashoffset: rectDasharray.value, }); }); }... the animate =() = > {
	// Loop animation
    this.state.rectDasharray.setValue(0);

    Animated.timing(this.state.rectDasharray, {
        toValue:
            (this.props.direction == 'right' ? -1 : 1) *
            this.state.allLength,
        duration: this.props.duration,
        easing: Easing.linear,
        useNativeDriver: true,
    }).start(this.animate);
};
Copy the code

SetNativeProps can imagine that you’re using native JS to set the style property of the DOM tag, which is the react-native tag that you’re writing. Dom is the Rect tag. Of course, you can also use either either Animated. Interpolate difference calculation, which I use stupidly because I’m lazy,