I don't think my use case should result in this level of animation dropouts, making the animation barely renderable.
import { Badge } from '@/components/ui/badge';
import { Text } from '@/components/ui/text';
import { getUsageColor, monitorCardStyles } from '~/components/styles/MonitorCardStyles';
import { useThemeColor } from '@/hooks/useThemeColor';
import { useTranslation } from '@/hooks/useTranslation';
import { useMonitoringStatus } from '@/lib/store';
import { ThreeXUIConfig } from '@/lib/types';
import { ArrowDown, ArrowUp } from 'lucide-react-native';
import React from 'react';
import { View } from 'react-native';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import { Pie, PolarChart } from 'victory-native';
import { SkeletonCard } from '~/components/SkeletonCard';
interface MonitorCardProps {
config: ThreeXUIConfig;
onPress?: (config: ThreeXUIConfig) => void;
onLongPress?: (config: ThreeXUIConfig) => void;
}
interface AnimatedPieChartProps {
data: Array<{ label: string; value: number; color: string }>;
size: number;
innerRadius?: number | string;
}
function AnimatedPieSlice({ slice }: { slice: any }) {
return (
<Pie.Slice
animate={{
type:'timing',
duration:300
}}
/>
);
}
function AnimatedPieChart({ data, size, innerRadius = "85%" }: AnimatedPieChartProps) {
return (
<View style={{ width: size, height: size+5 }}>
<PolarChart
data={data}
labelKey="label"
valueKey="value"
colorKey="color"
>
<Pie.Chart innerRadius={innerRadius}>
{({ slice }) => <AnimatedPieSlice slice={slice} />}
</Pie.Chart>
</PolarChart>
</View>
);
}
function SimpleThreeXUIMonitorCard({ config, onPress, onLongPress }: MonitorCardProps) {
const { t } = useTranslation();
const backgroundColor = useThemeColor({}, 'background');
const borderColor = useThemeColor({}, 'border');
const textColor = useThemeColor({}, 'text');
const status = useMonitoringStatus(config.id);
const tapGesture = Gesture.Tap().runOnJS(true)
.onStart(() => {
if (onPress) {
onPress(config);
}
});
const longPressGesture = Gesture.LongPress().runOnJS(true)
.onStart(() => {
if (onLongPress) {
onLongPress(config);
}
});
const combinedGesture = Gesture.Exclusive(longPressGesture, tapGesture);
if (!status) {
return (
<GestureDetector gesture={combinedGesture}>
<SkeletonCard />
</GestureDetector>
);
}
const formatUptime = (seconds: number): string => {
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
if (days > 0) {
return `${days}d ${hours}h`;
} else if (hours > 0) {
return `${hours}h ${minutes}m`;
} else {
return `${minutes}m`;
}
};
const formatBytes = (bytes: number): string => {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
};
const formatSpeed = (bytesPerSecond: number): string => {
return formatBytes(bytesPerSecond) + '/s';
};
const safeNumber = (value: any): number => {
if (typeof value === 'number' && !isNaN(value) && isFinite(value)) {
return value;
}
return 0;
};
return (
<GestureDetector gesture={combinedGesture}>
<View style={[monitorCardStyles.card, { backgroundColor, borderColor }]}>
<View style={monitorCardStyles.header}>
<View style={monitorCardStyles.titleContainer}>
<View style={monitorCardStyles.titleRow}>
<Text style={[monitorCardStyles.title, { color: textColor }]}>{config.name}</Text>
<Badge variant="secondary" style={monitorCardStyles.ipBadge}>
<Text style={monitorCardStyles.ipBadgeText}>3X-UI</Text>
</Badge>
</View>
</View>
<Badge
variant={status.isOnline ? "default" : "destructive"}
style={monitorCardStyles.statusBadge}
>
<Text style={monitorCardStyles.badgeText}>
{status.isOnline
? status.serverStatus ? formatUptime(status.serverStatus.uptime) : 'Online'
: 'Offline'
}
</Text>
</Badge>
</View>
<View style={monitorCardStyles.chartsContainer}>
<View style={monitorCardStyles.chartItem}>
<View style={monitorCardStyles.pieContainer}>
<AnimatedPieChart
data={status.serverStatus ? [
{
label: 'Used',
value: Math.max(safeNumber(status.serverStatus.cpu), 0.1),
color: getUsageColor(safeNumber(status.serverStatus.cpu))
},
{
label: 'Free',
value: 100 - Math.max(safeNumber(status.serverStatus.cpu), 0.1),
color: '#e5e7eb'
}
] : [
{
label: 'No Data',
value: 1,
color: '#f3f4f6'
},{
label: 'No Data',
value: 100,
color: '#f3f4f6'
}
]}
size={90}
/>
<View style={monitorCardStyles.chartInnerContent}>
<Text style={[monitorCardStyles.chartInnerLabel, { color: textColor }]}>CPU</Text>
<Text style={[monitorCardStyles.percentageText, { color: textColor }]}>
{status.serverStatus ? `${safeNumber(status.serverStatus.cpu).toFixed(1)}%` : 'No data'}
</Text>
</View>
</View>
</View>
<View style={monitorCardStyles.chartItem}>
<View style={monitorCardStyles.pieContainer}>
<AnimatedPieChart
data={status.serverStatus ? (() => {
const memCurrent = safeNumber(status.serverStatus.mem.current);
const memTotal = safeNumber(status.serverStatus.mem.total);
const memPercentage = memTotal > 0 ? (memCurrent / memTotal) * 100 : 0;
return [
{
label: 'Used',
value: Math.max(memPercentage, 0.1),
color: getUsageColor(memPercentage)
},
{
label: 'Free',
value: 100 - Math.max(memPercentage, 0.1),
color: '#e5e7eb'
}
];
})() : [
{
label: 'No Data',
value: 1,
color: '#f3f4f6'
},{
label: 'No Data',
value: 100,
color: '#f3f4f6'
}
]}
size={90}
/>
<View style={monitorCardStyles.chartInnerContent}>
<Text style={[monitorCardStyles.chartInnerLabel, { color: textColor }]}>Memory</Text>
<Text style={[monitorCardStyles.percentageText, { color: textColor }]}>
{status.serverStatus ? (() => {
const memCurrent = safeNumber(status.serverStatus.mem.current);
const memTotal = safeNumber(status.serverStatus.mem.total);
const memPercentage = memTotal > 0 ? (memCurrent / memTotal) * 100 : 0;
return `${memPercentage.toFixed(1)}%`;
})() : 'No data'}
</Text>
</View>
</View>
</View>
<View style={monitorCardStyles.chartItem}>
<View style={monitorCardStyles.pieContainer}>
<AnimatedPieChart
data={status.serverStatus ? (() => {
const diskCurrent = safeNumber(status.serverStatus.disk.current);
const diskTotal = safeNumber(status.serverStatus.disk.total);
const diskPercentage = diskTotal > 0 ? (diskCurrent / diskTotal) * 100 : 0;
return [
{
label: 'Used',
value: Math.max(diskPercentage, 0.1),
color: getUsageColor(diskPercentage)
},
{
label: 'Free',
value: 100 - Math.max(diskPercentage, 0.1),
color: '#e5e7eb'
}
];
})() : [
{
label: 'No Data',
value: 1,
color: '#f3f4f6'
},{
label: 'No Data',
value: 100,
color: '#f3f4f6'
}
]}
size={90}
/>
<View style={monitorCardStyles.chartInnerContent}>
<Text style={[monitorCardStyles.chartInnerLabel, { color: textColor }]}>Disk</Text>
<Text style={[monitorCardStyles.percentageText, { color: textColor }]}>
{status.serverStatus ? (() => {
const diskCurrent = safeNumber(status.serverStatus.disk.current);
const diskTotal = safeNumber(status.serverStatus.disk.total);
const diskPercentage = diskTotal > 0 ? (diskCurrent / diskTotal) * 100 : 0;
return `${diskPercentage.toFixed(1)}%`;
})() : 'No data'}
</Text>
</View>
</View>
</View>
</View>
<View style={monitorCardStyles.networkStats}>
{status.serverStatus ? (
<View style={monitorCardStyles.networkTrafficRow}>
<View style={monitorCardStyles.networkTrafficItem}>
<ArrowUp strokeWidth={3} size={14} color={textColor} style={monitorCardStyles.networkIcon} />
<Text style={[monitorCardStyles.networkTrafficText, { color: textColor }]}>
{formatSpeed(safeNumber(status.serverStatus.netIO.up))} {formatBytes(safeNumber(status.serverStatus.netTraffic.sent))}
</Text>
</View>
<View style={monitorCardStyles.networkTrafficItem}>
<ArrowDown strokeWidth={3} size={14} color={textColor} style={monitorCardStyles.networkIcon} />
<Text style={[monitorCardStyles.networkTrafficText, { color: textColor }]}>
{formatSpeed(safeNumber(status.serverStatus.netIO.down))} {formatBytes(safeNumber(status.serverStatus.netTraffic.recv))}
</Text>
</View>
</View>
) : (
<View style={monitorCardStyles.networkTrafficRow}>
<View style={monitorCardStyles.networkTrafficItem}>
<ArrowUp strokeWidth={3} size={14} color={textColor} style={monitorCardStyles.networkIcon} />
<Text style={[monitorCardStyles.networkTrafficText, { color: textColor }]}>No data</Text>
</View>
<View style={monitorCardStyles.networkTrafficItem}>
<ArrowDown strokeWidth={3} size={14} color={textColor} style={monitorCardStyles.networkIcon} />
<Text style={[monitorCardStyles.networkTrafficText, { color: textColor }]}>No data</Text>
</View>
</View>
)}
</View>
</View>
</GestureDetector>
);
}
export default React.memo(SimpleThreeXUIMonitorCard, (prev, next) => {
return (
prev.config.id === next.config.id &&
prev.config.name === next.config.name &&
prev.onPress === next.onPress &&
prev.onLongPress === next.onLongPress
);
});
"react-native": "0.79.5",
"@shopify/react-native-skia": "^2.2.3",
"react-native-reanimated": "~3.17.4",
"victory-native": "^41.19.2"
Question
I don't think my use case should result in this level of animation dropouts, making the animation barely renderable.
WhatsApp.Video.2025-10-02.at.13.11.02.mp4
Background Info/Attempts
This is my code:
This is the dependency version: