Я работаю над проектом 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;
Я нарисовал простой 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;
выглядит хорошо, но процентное соотношение, похоже, работает не так, как предполагалось. Граница должна функционировать как индикатор выполнения.
Установите атрибут pathLength = "100"
для <rect>
, чтобы работать с процентами для strokeDashArray
вместо вычислений: pathLength MDN
@AudunHilden Проверьте обновленный код, пожалуйста.
@Danny'365CSI'Engelman Вот так, приятель!
Я думал, что собственный SVG не поддерживает pathLength
Я просто понял, что это все-таки не сработало. Я забыл выбрать «IOS», когда пробовал ваш пример, хотя для раздела «Интернет» он работает: Snack.expo.dev/@audn/progress-border-v2
Я создал кое-что, что работает для реагировать на родные лыжи . Если вы рисуете границу, используя путь, вы можете использовать опору 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 вне браузера все работает
Есть ли у вас изображение того, как это должно выглядеть? Это круг, когда я импортирую его в свой код
Если ваш viewBox имеет ширину 70, а ваш круг имеет cx=35 и радиус 35, тогда любой штрих выйдет за пределы viewBox. То же самое и с высотой.