Граница прогресса вокруг эллиптического или округленно-прямоугольного представления в React Native

Я работаю над проектом React Native, где мне нужно отобразить индикатор прогресса, очерчивающий некруглый вид.

Я попробовал использовать response-native-svg с компонентом Circle, чтобы создать круговой индикатор прогресса, но это сработало не так, как я хотел.

Мне нужно, чтобы индикатор прогресса имел эллиптическую или округленно-прямоугольную форму.

Вот упрощенная версия моего текущего подхода с использованием базовых компонентов React Native: https://snack.expo.dev/@audn/progress-border

Что я пытаюсь сделать:

Что у меня есть на данный момент:

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;

Если ваш viewBox имеет ширину 70, а ваш круг имеет cx=35 и радиус 35, тогда любой штрих выйдет за пределы viewBox. То же самое и с высотой.

Robert Longson 25.08.2024 18:00
Умерло ли Create-React-App?
Умерло ли Create-React-App?
В этом документальном фильме React.dev мы исследуем, мертв ли Create React App (CRA) и какое будущее ждет этот популярный фреймворк React.
Освоение React Native: Пошаговое руководство для начинающих
Освоение React Native: Пошаговое руководство для начинающих
React Native - это популярный фреймворк с открытым исходным кодом, используемый для разработки мобильных приложений. Он был разработан компанией...
В чем разница между react native и react ?
В чем разница между react native и react ?
React и React Native - два популярных фреймворка для создания пользовательских интерфейсов, но они предназначены для разных платформ. React - это...
От React к React Native: Руководство для начинающих по разработке мобильных приложений с использованием React
От React к React Native: Руководство для начинающих по разработке мобильных приложений с использованием React
Если вы уже умеете работать с React, создание мобильных приложений для iOS и Android - это новое приключение, в котором вы сможете применить свои...
Хуки React: что это такое и как их использовать
Хуки React: что это такое и как их использовать
Хуки React - это мощная функция библиотеки React, которая позволяет разработчикам использовать состояние и другие возможности React без написания...
0
1
52
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Я нарисовал простой SVG файл и просто подключил его к вашему коду... Вам кажется это решение приемлемым?

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;

ОБНОВЛЯТЬ

версия путидлины

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;

выглядит хорошо, но процентное соотношение, похоже, работает не так, как предполагалось. Граница должна функционировать как индикатор выполнения.

Audun Hilden 25.08.2024 18:58

Установите атрибут pathLength = "100" для <rect>, чтобы работать с процентами для strokeDashArray вместо вычислений: pathLength MDN

Danny '365CSI' Engelman 25.08.2024 19:38

@AudunHilden Проверьте обновленный код, пожалуйста.

Łukasz D. Mastalerz 25.08.2024 19:49

@Danny'365CSI'Engelman Вот так, приятель!

Łukasz D. Mastalerz 25.08.2024 20:32

Я думал, что собственный SVG не поддерживает pathLength

PhantomSpooks 26.08.2024 19:30

Я просто понял, что это все-таки не сработало. Я забыл выбрать «IOS», когда пробовал ваш пример, хотя для раздела «Интернет» он работает: Snack.expo.dev/@audn/progress-border-v2

Audun Hilden 26.08.2024 19:39

Я создал кое-что, что работает для реагировать на родные лыжи . Если вы рисуете границу, используя путь, вы можете использовать опору end, чтобы получить границу индикатора выполнения.

import { Canvas, Path, Skia } from '@shopify/react-native-skia';
import { ReactNode, useMemo, useState } from 'react';
import { LayoutRectangle, StyleSheet, View, ViewStyle } from 'react-native';
import { useDerivedValue, withTiming } from 'react-native-reanimated';

type BorderViewProps = {
  progress:number;
  contentContainerStyle?:ViewStyle;
  children:ReactNode;
  backgroundColor?:string;
  color?:string;
  borderWidth?:number
}

export default function BorderView({progress, contentContainerStyle,children,borderWidth=2}:BorderViewProps){
  // store children layout properties
  const [layout,setLayout] = useState<LayoutRectangle>({
    width:0,
    height:0,
    x:0,
    y:0
  })
  // store  border as path
  const path = useMemo(()=>{
    const p = Skia.Path.Make()
    // changing start position of path will change
    // where the progress bar starts
    p.moveTo(layout.width,layout.height)
    // draw oval
    p.addArc({
      // tried to remove clipping drawing does by subtracting borderWidth
      width:layout.width-borderWidth,
      height:layout.height-borderWidth,
      x:0,
      y:0
    },0,360)
    p.close()
    return p
  },[layout,borderWidth])
  // use Path end property to animate progress
  const end = useDerivedValue(()=>withTiming(progress,{duration:200}))
  
  return (
    <>
      <Canvas style = {{
        // Canvas can only have skia elements within it
        // so position it absolutely and place non-skia elements
        // on top of it
        position:'absolute',
        left:layout.x,
        top:layout.y,
        width:layout.width,
        height:layout.height
        }}>
        <Path path = {path} style = "stroke" strokeWidth = {borderWidth} color = "orange" start = {0} end = {end}/>
      </Canvas>
      <View style = {[styles.contentContainer,contentContainerStyle]} onLayout = {e=>{
        const {width,height,x,y} = e.nativeEvent.layout
        setLayout({
          x,
          y,
          // attempt to to remove clipping
          width:width+borderWidth+2,
          height:height+borderWidth+2
        })}}>
        {children}
      </View>
    </>
  )
}

const styles= StyleSheet.create({
  container:{
    justifyContent:'center',
    alignItems: 'center',
    borderWidth:1
  },
  contentContainer:{
    padding:5,
    backgroundColor:'transparent'
  }
})

Использование:

import { StyleSheet, Text, View } from 'react-native';

import BorderView from '@/components/ProgressBorder';
import useProgressSimulation from '@/hooks/useProgressSimulation';



export default function HomeScreen() {
 const progress = useProgressSimulation(0.1)
  return (
    <View style = {styles.container}>
      <Text>This is the home page</Text>
      <BorderView progress = {progress}>
        <View style = {styles.calendar}>
        <Text>Monday</Text>
        <Text>5</Text>
        </View>
      </BorderView>
    </View>
  );
}

const styles = StyleSheet.create({
 container:{
  flex:1,
  justifyContent: 'center',
  alignItems: 'center',
  padding:10,
  backgroundColor:'white'
 },
 calendar:{
  justifyContent: 'center',
  alignItems: 'center',
 }
});

Хук для имитации изменения значения прогресса

import { useEffect, useState } from 'react'

const wait = (duration=100)=>{
  return new Promise(resolve=>setTimeout(()=>resolve(null), duration))
}
const getRandom=(min=0,max=1)=>{
  const range = max - min
  return Math.random()*range+min
}

export default function useProgressSimulation(initVal=0){
  const [progress,setProgress] = useState(initVal)
  useEffect(()=>{
    const interval = setInterval(()=>{
      // wait 100-500ms and then add random value to progress
      wait(getRandom(100,500)).then(()=>setProgress(prev=>{
        const newVal = prev+getRandom(0.1,0.25)
        if (newVal > 1){
          clearInterval(interval)
          return 1
        }
        return newVal
      }))
    },1000)
    return ()=>clearInterval(interval)
  },[])
  return progress
}

Я пытался сделать закуску для демо, но мне постоянно выдавалась ошибка об использовании SharedValues. При использовании expo вне браузера все работает

Есть ли у вас изображение того, как это должно выглядеть? Это круг, когда я импортирую его в свой код

Audun Hilden 26.08.2024 20:41

Другие вопросы по теме