I have a working circle timer in an expo snack. It's made up of 2 timers, when one finishes the next one starts and they go in loop. Each one has it's independent time.
I would like to add an initial time so they start running at "a clock time" of my preference.
import * as React from 'react';
import { Text, View, StyleSheet, Animated, Button } from 'react-native';
import Constants from 'expo-constants';
import { CountdownCircleTimer } from 'react-native-countdown-circle-timer';
const duration = [10, 20];
export default function App() {
const [isPlaying, setIsPlaying] = React.useState(true);
const [timeIndex, setTimeIndex] = React.useState(0);
return (
<View style={styles.container}>
<CountdownCircleTimer
key={timeIndex}
isPlaying={isPlaying}
duration={duration[timeIndex]}
colors={[
['#FFFF00', 0.4],
['#0000ff', 0.4],
]}
onComplete={() => {
setTimeIndex((index) => {
let newIndex = index + 1;
if (newIndex >= duration.length) {
newIndex = 0;
}
return newIndex;
});
return [true];
}}>
{({ remainingTime, animatedColor }) => (
<Animated.Text style={{ color: animatedColor, fontSize: 40 }}>
{remainingTime}
</Animated.Text>
)}
</CountdownCircleTimer>
<Button
title="Toggle Playing"
onPress={() => setIsPlaying((prev) => !prev)}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
paddingTop: Constants.statusBarHeight,
backgroundColor: '#ecf0f1',
padding: 8,
},
});
I created a hook useClock
that has configurability for how often its interval is ran, when the interval should start, and provides an upper limit for much time can pass between an interval and the start time:
import { useState, useRef, useEffect } from 'react';
import { convertDate } from '../helpers/time';
export default function useClock({
intervalInSeconds = 1,
atMoment,
startTime,
maxTimeElaspedInSeconds = 15,
}) {
const hasStarted = useRef(false);
// hook doesnt need to return a time but I was using this state for debugging
const [time, setTime] = useState(new Date());
useEffect(() => {
const interval = setInterval(() => {
const newTime = new Date();
const timeDiff = newTime.getTime() - startTime.getTime();
// if waiting for next interval would call atMoment too late
// call atMoment in current interval
const shouldRun =
timeDiff + intervalInSeconds * 1000 > maxTimeElaspedInSeconds * 1000;
if (shouldRun && !hasStarted.current) {
console.log('Running at', convertDate(newTime));
hasStarted.current = true;
atMoment();
}
setTime(newTime);
}, intervalInSeconds * 1000);
return () => {
clearInterval(interval);
hasStarted.current = false;
};
}, [atMoment, startTime, intervalInSeconds, maxTimeElaspedInSeconds]);
return time;
}
With this you run a function a specify time:
import { Text, SafeAreaView, StyleSheet, Button, Animated } from 'react-native';
import useClock from './hooks/useClock';
import { useRef, useState, useEffect } from 'react';
import { createFutureDateInMinutes,convertDate } from './helpers/time';
import { CountdownCircleTimer } from 'react-native-countdown-circle-timer';
const duration = [10,20]
const intervalInSeconds = 15
export default function App() {
// todo: you will have to convert time string into a date object
// for testing purposes I just set the start time to a 30 seconds
// after the first render
const startTime = useRef(createFutureDateInMinutes(30/60)).current;
const [isPlaying, setIsPlaying] = useState(false);
const [timeIndex, setTimeIndex] = useState(0);
// useTime tries to estimate what the next
const time = useClock({
startTime,
// maximum time allowed to elaspe before calling atMoment
maxTimeElaspedInSeconds:1,
// Im not sure of performance effect of running an interval
// every second has, but depending on the complexity of your
// app you might need to make this variable as big as possible
// this hook guesses how much time will pass before its called again
// and if it exceeds maxTimeElaspedInSeconds, will call the hook earlier
intervalInSeconds,
atMoment: () => {
console.log('start time reached')
setIsPlaying(true);
},
});
return (
<SafeAreaView style={styles.container}>
<Text>Start time:{convertDate(startTime)}</Text>
<Text>Current time:{convertDate(time)}</Text>
<CountdownCircleTimer
key={timeIndex}
isPlaying={isPlaying}
duration={duration[timeIndex]}
colors={[
['#FFFF00', 0.4],
['#0000ff', 0.4],
]}
onComplete={() => {
setTimeIndex((index) => {
let newIndex = index + 1;
if (newIndex >= duration.length) {
newIndex = 0;
}
return newIndex;
});
return [true];
}}>
{({ remainingTime, animatedColor }) => (
<Animated.Text style={{ color: animatedColor, fontSize: 40 }}>
{remainingTime}
</Animated.Text>
)}
</CountdownCircleTimer>
<Button
title="Toggle Playing"
onPress={() => setIsPlaying((prev) => !prev)}
/>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
backgroundColor: '#ecf0f1',
padding: 8,
},
paragraph: {
margin: 24,
fontSize: 18,
fontWeight: 'bold',
textAlign: 'center',
},
});