react-nativesvgborderreact-native-svg

Progress border around an Elliptical or Rounded-Rectangular View in React Native


I'm working on a React Native project where I need to display a progress indicator that outlines a non-circular view.

I tried using react-native-svg with the Circle component to create a circular progress indicator, but it didn't work as I wanted.

I need the progress indicator to fit an elliptical or rounded-rectangular shape.

Here’s a simplified version of my current approach using basic React Native components: https://snack.expo.dev/@audn/progress-border

What I'm trying to make:

Goal

What I have so far:

Current

import { TouchableOpacity, Text, View, StyleSheet } from 'react-native';
import moment from 'moment';
import Svg, { Circle } from 'react-native-svg';

const DateComponent = () => {
  const date = moment(new Date());
  const dayName = date.format('dd').charAt(0);
  const dayNumber = date.format('D');
  const isFutureDate = date.isAfter(moment(), 'day');

  const progress = 0.75;
  const radius = 35;
  const strokeWidth = 2;
  const circumference = 2 * Math.PI * radius;

  return (
    <TouchableOpacity  style={styles.container}>
      <View style={styles.wrapper}>
        <Svg height="70" width="70" viewBox="0 0 70 70">
          <Circle
            cx="35" 
            cy="35"
            r={radius}
            stroke="gray"
            strokeWidth={strokeWidth}
            fill="none"
            opacity={0.2}
          />
          <Circle
            cx="35"
            cy="35"
            r={radius}
            stroke="green"
            strokeWidth={strokeWidth}
            fill="none"
            strokeDasharray={`${circumference} ${circumference}`}
            strokeDashoffset={(1 - progress) * circumference}
            strokeLinecap="round"
            transform="rotate(-90, 35, 35)"
          />
        </Svg>

        <View style={styles.card}>
          <Text style={styles.dayText}>{dayName}</Text>
          <Text style={[styles.dateText, { color: isFutureDate ? '#757575' : 'black' }]}>
            {dayNumber}
          </Text>
        </View>
      </View>
    </TouchableOpacity>
  );
};

const styles = StyleSheet.create({
  container: {
    justifyContent: 'center',
    alignItems: 'center',
  },
  wrapper: {
    justifyContent: 'center',
    alignItems: 'center',
  },
  card: {
    position: 'absolute',
    top: 0,
    left: 0,
    right: 0,
    bottom: 0,
    justifyContent: 'center',
    alignItems: 'center',
    borderRadius: 35,
    height: 70,
    width: 70,
  },
  dayText: {
    fontSize: 14,
    color: '#757575',
  },
  dateText: {
    fontSize: 18,
    fontWeight: 'bold',
  },
});

export default DateComponent;

Solution

  • I drew a simple SVG file and just switched in it to your code... Does this solution seem ok to you?

    import React from 'react';
    import { TouchableOpacity, StyleSheet, View } from 'react-native';
    import Svg, { Rect, Text as SvgText } from 'react-native-svg';
    import moment from 'moment';
    
    const DateComponent = () => {
      const date = moment(new Date());
      const dayName = date.format('dd').charAt(0);
      const dayNumber = date.format('D');
      const isFutureDate = date.isAfter(moment(), 'day');
    
      const progress = 0.50;
      const radius = 30;
      const strokeWidth = 3;
      const rectWidth = 60;
      const rectHeight = 80;
      const perimeter = 1.6 * (rectWidth + rectHeight);
      const progressLength = progress * perimeter;
    
      return (
        <View style={styles.screen}>
          <TouchableOpacity style={styles.container}>
            <Svg width="70" height="100" viewBox="0 0 70 100">
              <Rect
                x="5"
                y="10"
                width={rectWidth}
                height={rectHeight}
                rx={radius}
                ry={radius}
                fill="black"
                stroke="gray"
                strokeWidth={strokeWidth}
                opacity={0.2}
              />
              <Rect
                x="5"
                y="10"
                width={rectWidth}
                height={rectHeight}
                rx={radius}
                ry={radius}
                fill="none"
                stroke="green"
                strokeWidth={strokeWidth}
                strokeDasharray={`${progressLength} ${perimeter - progressLength}`}
                strokeLinecap="round"
              />
              <SvgText
                x="35"
                y="40"
                textAnchor="middle"
                fill="#858585"
                fontSize="20"
                fontFamily="Arial"
              >
                {dayName}
              </SvgText>
              <SvgText
                x="35"
                y="75"
                textAnchor="middle"
                fill={isFutureDate ? '#757575' : 'white'}
                fontSize="20"
                fontFamily="Arial"
              >
                {dayNumber}
              </SvgText>
            </Svg>
          </TouchableOpacity>
        </View>
      );
    };
    
    const styles = StyleSheet.create({
      screen: {
        flex: 1,
        justifyContent: 'center',
        alignItems: 'center',
        backgroundColor: '#f5f5f5',
      },
      container: {
        justifyContent: 'center',
        alignItems: 'center',
        width: 70,
        height: 100,
      },
    });
    
    export default DateComponent;
    

    UPDATE

    pathLength version

    import React from 'react';
    import { TouchableOpacity, StyleSheet, View } from 'react-native';
    import Svg, { Rect, Text as SvgText } from 'react-native-svg';
    import moment from 'moment';
    
      const DateComponent = () => {
      const date = moment(new Date());
      const dayName = date.format('dd').charAt(0);
      const dayNumber = date.format('D');
      const isFutureDate = date.isAfter(moment(), 'day');
    
      const progress = 0.50;
      const radius = 30;
      const strokeWidth = 3;
      const rectWidth = 60;
      const rectHeight = 80;
    
      return (
        <View style={styles.screen}>
          <TouchableOpacity style={styles.container}>
            <Svg width="70" height="100" viewBox="0 0 70 100">
              <Rect
                x="5"
                y="10"
                width={rectWidth}
                height={rectHeight}
                rx={radius}
                ry={radius}
                fill="black"
                stroke="gray"
                strokeWidth={strokeWidth}
                opacity={0.2}
              />
              <Rect
                x="5"
                y="10"
                width={rectWidth}
                height={rectHeight}
                rx={radius}
                ry={radius}
                fill="none"
                stroke="green"
                strokeWidth={strokeWidth}
                strokeDasharray={`${progress * 100} ${100 - (progress * 100)}`}
                strokeLinecap="round"
                pathLength="100"
              />
              <SvgText
                x="35"
                y="40"
                textAnchor="middle"
                fill="#858585"
                fontSize="20"
                fontFamily="Arial"
              >
                {dayName}
              </SvgText>
              <SvgText
                x="35"
                y="75"
                textAnchor="middle"
                fill={isFutureDate ? '#757575' : 'white'}
                fontSize="20"
                fontFamily="Arial"
              >
                {dayNumber}
              </SvgText>
            </Svg>
          </TouchableOpacity>
        </View>
      );
    };
    
    const styles = StyleSheet.create({
      screen: {
        flex: 1,
        justifyContent: 'center',
        alignItems: 'center',
        backgroundColor: '#f5f5f5',
      },
      container: {
        justifyContent: 'center',
        alignItems: 'center',
        width: 70,
        height: 100,
      },
    });
    
    export default DateComponent;