reverting to chase a bug w expo

This commit is contained in:
Chris Sanden
2026-04-24 22:48:43 +02:00
parent b191a1eced
commit c68aeba709

View File

@@ -23,11 +23,7 @@ const ITEM_SIZE = width * 0.38;
const ITEM_SPACING = (width - ITEM_SIZE) / 2; const ITEM_SPACING = (width - ITEM_SIZE) / 2;
const TIMER_UNIT_IN_SECONDS = 60; // Set to 60 for timer value to represent minutes const TIMER_UNIT_IN_SECONDS = 60; // Set to 60 for timer value to represent minutes
const HOLD_TO_CANCEL_MS = 2000; const HOLD_TO_CANCEL_MS = 2000;
const CANCEL_ANIMATION_DELAY_MS = 250; const CANCEL_ANIMATION_DELAY_MS = 250
const CANCEL_RELEASE_MS = 750;
const START_TRANSITION_MS = 300;
const TIMER_RESET_MS = 300;
const COUNTDOWN_FADE_MS = 180;
const placeholderTask = { const placeholderTask = {
name: 'Read chapter 4', name: 'Read chapter 4',
description: 'Focus on the summary questions and write down anything unclear.', description: 'Focus on the summary questions and write down anything unclear.',
@@ -41,72 +37,44 @@ function formatTime(totalSeconds: number) {
seconds.toString().padStart(2, '0')}`; seconds.toString().padStart(2, '0')}`;
} }
function getCancelOverlayTarget({
containerHeight,
holdStartedAt,
sessionStartedAt,
sessionDurationMs,
}: {
containerHeight: number;
holdStartedAt: number;
sessionStartedAt: number;
sessionDurationMs: number;
}) {
const now = Date.now();
const elapsedHoldMs = now - holdStartedAt;
const remainingHoldMs = Math.max(1, HOLD_TO_CANCEL_MS - elapsedHoldMs);
const elapsedAtCancelMs = now + remainingHoldMs - sessionStartedAt;
const expectedProgress = elapsedAtCancelMs / sessionDurationMs;
const clampedProgress = Math.max(0, Math.min(expectedProgress, 1));
const expectedYAtCancel = containerHeight * clampedProgress;
const cancelOffset = Math.max(0, containerHeight - expectedYAtCancel);
return { cancelOffset, remainingHoldMs };
}
export default function App() { export default function App() {
const [containerHeight, setContainerHeight] = React.useState(0); const [containerHeight, setContainerHeight] = React.useState(0)
const [duration, setDuration] = React.useState(timers[0]); const [duration, setDuration] = React.useState(timers[0])
const [timerIsRunning, setIsRunning] = React.useState(false); const [timerIsRunning, setIsRunning] = React.useState(false)
const [timeRemaining, setTimeRemaining] = React.useState(0); const [timeRemaining, setTimeRemaining] = React.useState(0)
// Animated values
const scrollX = React.useRef(new Animated.Value(0)).current; const scrollX = React.useRef(new Animated.Value(0)).current;
const timerAnimation = React.useRef(new Animated.Value(0)).current; const timerAnimation = React.useRef(new Animated.Value(0)).current;
const buttonAnimation = React.useRef(new Animated.Value(0)).current; const buttonAnimation = React.useRef(new Animated.Value(0)).current;
const taskDetailsAnimation = React.useRef(new Animated.Value(0)).current; const taskDetailsAnimation = React.useRef(new Animated.Value(0)).current;
const countdownAnimation = React.useRef(new Animated.Value(0)).current; const countdownAnimation = React.useRef(new Animated.Value(0)).current;
const cancelButtonAnimation = React.useRef(new Animated.Value(0)).current;
const pressedButtonAnimation = React.useRef(new Animated.Value(0)).current;
const focusModeAnimation = React.useRef(new Animated.Value(0)).current; // 0 = timer inactive, 1 = timer active
const timerOverlayOpacity = React.useRef(new Animated.Value(1)).current;
const cancelOverlayAnimation = React.useRef(new Animated.Value(0)).current;
// Timer/session refs
const countdownRef = React.useRef<ReturnType<typeof setInterval> | null>(null); const countdownRef = React.useRef<ReturnType<typeof setInterval> | null>(null);
const cancelHoldTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
const runningAnimationRef = React.useRef<Animated.CompositeAnimation | null>(null); const runningAnimationRef = React.useRef<Animated.CompositeAnimation | null>(null);
const progressAnimationRef = React.useRef<Animated.CompositeAnimation | null>(null); const progressAnimationRef = React.useRef<Animated.CompositeAnimation | null>(null);
const sessionStartedAtRef = React.useRef<number | null>(null); const sessionStartedAtRef = React.useRef<number | null>(null);
const sessionDurationMsRef = React.useRef(0); const sessionDurationMsRef = React.useRef(0);
const cancelButtonAnimation = React.useRef(new Animated.Value(0)).current;
// Cancel-hold refs const pressedButtonAnimation = React.useRef(new Animated.Value(0)).current;
const cancelHoldTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null); const focusModeAnimation = React.useRef(new Animated.Value(0)).current; // 0 = timer inactive, 1 = timer active
const cancelHoldAnimationDelayRef = React.useRef<ReturnType<typeof setTimeout> | null>(null); const cancelHoldAnimationDelayRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
const cancelAccelStartedRef = React.useRef(false); const cancelAccelStartedRef = React.useRef(false);
const cancelHoldActiveRef = React.useRef(false); const cancelHoldActiveRef = React.useRef(false);
const cancelHoldIdRef = React.useRef(0); const cancelHoldIdRef = React.useRef(0);
const cancelHoldStartedAtRef = React.useRef(0); const cancelHoldStartedAtRef = React.useRef(0);
const cancelOverlayAnimation = React.useRef(new Animated.Value(0)).current;
React.useEffect(() => { React.useEffect(() => {
if (containerHeight > 0 && !timerIsRunning) { if (containerHeight > 0 && !timerIsRunning) {
timerAnimation.setValue(containerHeight); timerAnimation.setValue(containerHeight);
} }
}, [containerHeight, timerIsRunning, timerAnimation]); }, [containerHeight, timerIsRunning, timerAnimation]);
const timerOverlayOpacity = React.useRef(new Animated.Value(1)).current;
const cancelButtonOpacity = cancelButtonAnimation; const cancelButtonOpacity = cancelButtonAnimation;
const pressedButtonScale = pressedButtonAnimation.interpolate({ const pressedButtonScale = pressedButtonAnimation.interpolate({
inputRange: [0, 1], inputRange: [0, 1],
outputRange: [1, 0.90], outputRange: [1, 0.90],
}); });
@@ -139,60 +107,31 @@ export default function App() {
outputRange: [1, 0.55], outputRange: [1, 0.55],
}); });
const startButtonOpacity = buttonAnimation.interpolate({ const opacity = buttonAnimation.interpolate({
inputRange: [0, 1], inputRange: [0, 1],
outputRange: [1, 0] outputRange: [1, 0]
}); });
const startButtonTranslateY = buttonAnimation.interpolate({ const translateY = buttonAnimation.interpolate({
inputRange: [0, 1], inputRange: [0, 1],
outputRange: [0, 200] outputRange: [0, 200]
}); });
const inactiveTimerNumberOpacity = buttonAnimation.interpolate({ const inactiveTimerNumberOpacity = buttonAnimation.interpolate({
inputRange: [0, 1], inputRange: [0, 1],
outputRange: [1, 0] outputRange: [1, 0]
}); });
const taskDetailsOpacity = taskDetailsAnimation.interpolate({ const taskDetailsOpacity = taskDetailsAnimation.interpolate({
inputRange: [0, 1], inputRange: [0, 1],
outputRange: [0, 1] outputRange: [0, 1]
}); });
const taskDetailsTranslateY = taskDetailsAnimation.interpolate({ const taskDetailsTranslateY = taskDetailsAnimation.interpolate({
inputRange: [0, 1], inputRange: [0, 1],
outputRange: [20, 0] outputRange: [20, 0]
}); });
const clearCountdown = React.useCallback(() => {
if (countdownRef.current) {
clearInterval(countdownRef.current);
countdownRef.current = null;
}
}, []);
const clearCancelHoldTimers = React.useCallback(() => {
if (cancelHoldTimeoutRef.current) {
clearTimeout(cancelHoldTimeoutRef.current);
cancelHoldTimeoutRef.current = null;
}
if (cancelHoldAnimationDelayRef.current) {
clearTimeout(cancelHoldAnimationDelayRef.current);
cancelHoldAnimationDelayRef.current = null;
}
}, []);
const stopTimerAnimations = React.useCallback(() => {
runningAnimationRef.current?.stop();
runningAnimationRef.current = null;
progressAnimationRef.current?.stop();
progressAnimationRef.current = null;
cancelOverlayAnimation.stopAnimation();
}, [cancelOverlayAnimation]);
const animateButtonPress = React.useCallback((pressed: boolean) => { const animateButtonPress = React.useCallback((pressed: boolean) => {
Animated.timing(pressedButtonAnimation, { Animated.timing(pressedButtonAnimation, {
toValue: pressed ? 1 : 0, toValue: pressed ? 1 : 0,
@@ -202,11 +141,19 @@ export default function App() {
}, [pressedButtonAnimation]); }, [pressedButtonAnimation]);
const cancelTimer = React.useCallback(() => { const cancelTimer = React.useCallback(() => {
if (!timerIsRunning) { if (!timerIsRunning){
return; return;
} }
clearCountdown(); if (countdownRef.current) {
stopTimerAnimations(); clearInterval(countdownRef.current);
countdownRef.current = null;
}
runningAnimationRef.current?.stop();
runningAnimationRef.current = null;
progressAnimationRef.current?.stop();
progressAnimationRef.current = null;
cancelOverlayAnimation.stopAnimation();
Animated.parallel([ Animated.parallel([
Animated.timing(cancelButtonAnimation, { Animated.timing(cancelButtonAnimation, {
@@ -220,18 +167,18 @@ export default function App() {
useNativeDriver: true useNativeDriver: true
}), }),
Animated.timing(focusModeAnimation, { Animated.timing(focusModeAnimation, {
toValue: 0, toValue: 0,
duration: 250, duration: 250,
useNativeDriver: true useNativeDriver: true
}), }),
Animated.timing(countdownAnimation, { Animated.timing(countdownAnimation, {
toValue: 0, toValue: 0,
duration: COUNTDOWN_FADE_MS, duration: 180,
useNativeDriver: true useNativeDriver: true
}), }),
Animated.timing(timerAnimation, { Animated.timing(timerAnimation, {
toValue: containerHeight, toValue: containerHeight,
duration: TIMER_RESET_MS, duration: 300,
useNativeDriver: true useNativeDriver: true
}), }),
Animated.timing(timerOverlayOpacity, { Animated.timing(timerOverlayOpacity, {
@@ -244,24 +191,23 @@ export default function App() {
duration: 120, duration: 120,
useNativeDriver: true, useNativeDriver: true,
}) })
]).start(() => { ]).start(() => {
Animated.timing(buttonAnimation, { Animated.timing(buttonAnimation, {
toValue: 0, toValue: 0,
duration: 220, duration: 220,
useNativeDriver: true useNativeDriver: true
}) })
.start(() => { .start(() => {
timerAnimation.setValue(containerHeight); timerAnimation.setValue(containerHeight)
cancelOverlayAnimation.setValue(0); cancelOverlayAnimation.setValue(0);
timerOverlayOpacity.setValue(1); timerOverlayOpacity.setValue(1);
setTimeRemaining(0); setTimeRemaining(0);
setIsRunning(false); setIsRunning(false);
}); });
}) })
}, [timerOverlayOpacity, buttonAnimation, cancelButtonAnimation, }, [timerOverlayOpacity, buttonAnimation, cancelButtonAnimation,
cancelOverlayAnimation, clearCountdown, containerHeight, countdownAnimation, cancelOverlayAnimation, containerHeight, countdownAnimation, timerAnimation,
focusModeAnimation, stopTimerAnimations, taskDetailsAnimation, timerIsRunning, taskDetailsAnimation, focusModeAnimation]);
timerAnimation, timerIsRunning]);
const startCancelHold = React.useCallback(() => { const startCancelHold = React.useCallback(() => {
animateButtonPress(true); animateButtonPress(true);
@@ -281,12 +227,14 @@ export default function App() {
cancelAccelStartedRef.current = true; cancelAccelStartedRef.current = true;
cancelOverlayAnimation.setValue(0); cancelOverlayAnimation.setValue(0);
const { cancelOffset, remainingHoldMs } = getCancelOverlayTarget({ const elapsedHoldMs = Date.now() - cancelHoldStartedAtRef.current;
containerHeight, const remainingHoldMs = Math.max(1, HOLD_TO_CANCEL_MS - elapsedHoldMs);
holdStartedAt: cancelHoldStartedAtRef.current, const sessionStartedAt = sessionStartedAtRef.current ?? Date.now();
sessionStartedAt: sessionStartedAtRef.current ?? Date.now(), const elapsedAtCancelMs = Date.now() + remainingHoldMs - sessionStartedAt;
sessionDurationMs: sessionDurationMsRef.current, const expectedProgress = elapsedAtCancelMs / sessionDurationMsRef.current;
}); const clampedProgress = Math.max(0, Math.min(expectedProgress, 1));
const expectedYAtCancel = containerHeight * clampedProgress;
const cancelOffset = Math.max(0, containerHeight - expectedYAtCancel);
Animated.timing(cancelOverlayAnimation, { Animated.timing(cancelOverlayAnimation, {
toValue: cancelOffset, toValue: cancelOffset,
@@ -304,7 +252,7 @@ export default function App() {
cancelHoldActiveRef.current = false; cancelHoldActiveRef.current = false;
cancelHoldIdRef.current += 1; cancelHoldIdRef.current += 1;
cancelAccelStartedRef.current = false; cancelAccelStartedRef.current = false;
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning); Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning)
cancelTimer(); cancelTimer();
cancelHoldTimeoutRef.current = null; cancelHoldTimeoutRef.current = null;
}, HOLD_TO_CANCEL_MS); }, HOLD_TO_CANCEL_MS);
@@ -312,40 +260,40 @@ export default function App() {
const finishTimer = React.useCallback(() => { const finishTimer = React.useCallback(() => {
Animated.parallel([ Animated.parallel([
Animated.timing(countdownAnimation, { Animated.timing(countdownAnimation, {
toValue: 0,
duration: 200,
useNativeDriver: true
}),
Animated.timing(focusModeAnimation, {
toValue: 0,
duration: 250,
useNativeDriver: true
}),
Animated.timing(taskDetailsAnimation, {
toValue: 0,
duration: 300,
useNativeDriver: true
})
]).start(() => {
Animated.parallel([
Animated.timing(buttonAnimation, {
toValue: 0, toValue: 0,
duration: START_TRANSITION_MS, duration: 200,
useNativeDriver: true useNativeDriver: true
}), }),
Animated.timing(cancelButtonAnimation, { Animated.timing(focusModeAnimation, {
toValue: 0, toValue: 0,
duration: START_TRANSITION_MS, duration: 250,
useNativeDriver: true useNativeDriver: true
}), }),
Animated.timing(taskDetailsAnimation, {
toValue: 0,
duration: 300,
useNativeDriver: true
})
]).start(() => { ]).start(() => {
setIsRunning(false); Animated.parallel([
Animated.timing(buttonAnimation, {
toValue: 0,
duration: 300,
useNativeDriver: true
}),
Animated.timing(cancelButtonAnimation, {
toValue: 0,
duration: 300,
useNativeDriver: true
}),
]).start(() => {
setIsRunning(false);
/* TODO /* TODO
Implement store and send of ellapsed time value in seconds to DB Implement store and send of ellapsed time value in seconds to DB
for total time spent statistic for total time spent statistic
*/ */
}) })
}) })
}, [countdownAnimation, focusModeAnimation, taskDetailsAnimation, buttonAnimation, cancelButtonAnimation]); }, [countdownAnimation, focusModeAnimation, taskDetailsAnimation, buttonAnimation, cancelButtonAnimation]);
@@ -380,7 +328,15 @@ export default function App() {
cancelHoldActiveRef.current = false; cancelHoldActiveRef.current = false;
cancelHoldIdRef.current += 1; cancelHoldIdRef.current += 1;
clearCancelHoldTimers(); if (cancelHoldTimeoutRef.current) {
clearTimeout(cancelHoldTimeoutRef.current);
cancelHoldTimeoutRef.current = null;
}
if (cancelHoldAnimationDelayRef.current) {
clearTimeout(cancelHoldAnimationDelayRef.current);
cancelHoldAnimationDelayRef.current = null;
}
if (!cancelAccelStartedRef.current) { if (!cancelAccelStartedRef.current) {
return; return;
@@ -391,18 +347,18 @@ export default function App() {
cancelOverlayAnimation.setValue(currentOffset); cancelOverlayAnimation.setValue(currentOffset);
Animated.timing(cancelOverlayAnimation, { Animated.timing(cancelOverlayAnimation, {
toValue: 0, toValue: 0,
duration: CANCEL_RELEASE_MS, duration: 750,
easing: Easing.in(Easing.bounce), easing: Easing.in(Easing.bounce),
useNativeDriver: true useNativeDriver: true
}).start(); }).start();
}); });
}, [animateButtonPress, cancelOverlayAnimation, clearCancelHoldTimers]); }, [animateButtonPress, cancelOverlayAnimation]);
const startTimer = React.useCallback(() => { const animation = React.useCallback(() => {
if (timerIsRunning || containerHeight === 0) { if (timerIsRunning || containerHeight === 0) {
return; return;
} }
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success)
setIsRunning(true); setIsRunning(true);
taskDetailsAnimation.setValue(0); taskDetailsAnimation.setValue(0);
countdownAnimation.setValue(0); countdownAnimation.setValue(0);
@@ -414,38 +370,44 @@ export default function App() {
sessionStartedAtRef.current = Date.now(); sessionStartedAtRef.current = Date.now();
sessionDurationMsRef.current = totalSeconds * 1000; sessionDurationMsRef.current = totalSeconds * 1000;
clearCountdown(); if (countdownRef.current) {
clearInterval(countdownRef.current);
countdownRef.current = null;
}
countdownRef.current = setInterval(() => { countdownRef.current = setInterval(() => {
setTimeRemaining((currentTime) => { setTimeRemaining((currentTime) => {
if (currentTime <= 1) { if (currentTime <= 1) {
clearCountdown(); if (countdownRef.current) {
clearInterval(countdownRef.current);
countdownRef.current = null;
}
return 0; return 0;
} }
return currentTime - 1; return currentTime -1;
}); });
}, 1000); }, 1000);
const runningAnimation = Animated.sequence([ const runningAnimation = Animated.sequence([
Animated.parallel([ Animated.parallel([
Animated.timing(buttonAnimation, { Animated.timing(buttonAnimation, {
toValue: 1, toValue: 1,
duration: START_TRANSITION_MS, duration: 300,
useNativeDriver: true useNativeDriver: true
}), }),
Animated.timing(cancelButtonAnimation, { Animated.timing(cancelButtonAnimation, {
toValue: 1, toValue: 1,
duration: START_TRANSITION_MS, duration: 300,
useNativeDriver: true useNativeDriver: true
}), }),
Animated.timing(countdownAnimation, { Animated.timing(countdownAnimation, {
toValue: 1, toValue: 1,
duration: START_TRANSITION_MS, duration: 300,
useNativeDriver: true useNativeDriver: true
}), }),
Animated.timing(timerAnimation, { Animated.timing(timerAnimation, {
toValue: 0, toValue: 0,
duration: START_TRANSITION_MS, duration: 300,
useNativeDriver: true useNativeDriver: true
}), }),
]), ]),
@@ -472,20 +434,24 @@ export default function App() {
startProgressAnimation(0); startProgressAnimation(0);
}); });
}, [cancelButtonAnimation, countdownAnimation, }, [cancelButtonAnimation, countdownAnimation,
buttonAnimation, cancelOverlayAnimation, clearCountdown, taskDetailsAnimation, buttonAnimation, cancelOverlayAnimation, taskDetailsAnimation,
timerAnimation, focusModeAnimation, duration, timerIsRunning, containerHeight, startProgressAnimation]); timerAnimation, focusModeAnimation, duration, timerIsRunning, containerHeight, startProgressAnimation]);
const renderTimerOverlay = () => ( return (
<View style={styles.container}
onLayout={(event) => {
setContainerHeight(event.nativeEvent.layout.height);
}}>
<StatusBar hidden />
<Animated.View <Animated.View
style={[StyleSheet.absoluteFillObject, { style={[StyleSheet.absoluteFillObject, {
height: containerHeight, height: containerHeight,
width, width,
backgroundColor: colors.red, backgroundColor: colors.red,
transform: [{ translateY: timerOverlayTranslateY }] transform: [{
}]} translateY: timerOverlayTranslateY
}]
}]}
/> />
);
const renderStartButton = () => (
<Animated.View <Animated.View
style={[ style={[
StyleSheet.absoluteFillObject, StyleSheet.absoluteFillObject,
@@ -493,71 +459,64 @@ export default function App() {
justifyContent: 'flex-end', justifyContent: 'flex-end',
alignItems: 'center', alignItems: 'center',
paddingBottom: 100, paddingBottom: 100,
opacity: startButtonOpacity, opacity,
transform: [{ translateY: startButtonTranslateY }] transform: [{
translateY
}]
}, },
]}> ]}>
<TouchableOpacity <TouchableOpacity
disabled={timerIsRunning} disabled={timerIsRunning}
onPress={startTimer} onPress={animation}
onPressIn={() => animateButtonPress(true)} onPressIn={() => animateButtonPress(true)}
onPressOut={() => animateButtonPress(false)}> onPressOut={() => animateButtonPress(false)}>
<Animated.View style={[ <Animated.View style={[styles.roundButton,
styles.roundButton,
{ {
transform: [{ scale: pressedButtonScale }], transform: [{ scale: pressedButtonScale }],
}, },
]}> ]}>
<Text className='text-text-main text-xl'>Start</Text> <Text className='text-text-main text-xl'>Start</Text>
<Text className='text-text-main text-xl'>Sprint</Text> <Text className='text-text-main text-xl'>Sprint</Text>
</Animated.View> </Animated.View>
</TouchableOpacity> </TouchableOpacity>
</Animated.View> </Animated.View>
);
const renderCancelButton = () => (
<Animated.View <Animated.View
pointerEvents={timerIsRunning? 'auto' : 'none'} pointerEvents={timerIsRunning? 'auto' : 'none'}
style={[ style={[
styles.cancelButtonContainer, styles.cancelButtonContainer,
{ {
opacity: cancelButtonOpacity, opacity: cancelButtonOpacity,
transform: [{translateY: cancelButtonTranslateY}], transform: [{translateY: cancelButtonTranslateY}],
}, },
]}> ]}>
<TouchableOpacity <TouchableOpacity
onPressIn={startCancelHold} onPressIn={startCancelHold}
onPressOut={stopCancelHold}> onPressOut={stopCancelHold}>
<Animated.View style={[ <Animated.View style={[styles.cancelButton,
styles.cancelButton,
{ {
transform: [{ scale: pressedButtonScale }], transform: [{ scale: pressedButtonScale }],
}, },
]}> ]}
>
<Text className='text-text-main text-xl'>Hold to end sprint</Text> <Text className='text-text-main text-xl'>Hold to end sprint</Text>
</Animated.View> </Animated.View>
</TouchableOpacity> </TouchableOpacity>
</Animated.View> </Animated.View>
);
const renderCountdownOverlay = () => (
<Animated.View <Animated.View
pointerEvents="none" pointerEvents= 'none'
style={[ style={[
styles.countdownOverlay, { styles.countdownOverlay, {
opacity: countdownAnimation, opacity: countdownAnimation,
transform: [ transform: [
{translateX: countdownTranslateX}, {translateX: countdownTranslateX},
{translateY: countdownTranslateY}, {translateY: countdownTranslateY},
{scale: countdownScale}, {scale: countdownScale},
], ],
}, },
]}> ]}
<Text style={styles.countdownText}>{formatTime(timeRemaining)}</Text> >
</Animated.View> <Text style= {styles.countdownText}>{formatTime(timeRemaining)}</Text>
); </Animated.View>
const renderDurationPicker = () => (
<View <View
style={{ style={{
position: 'absolute', position: 'absolute',
@@ -566,7 +525,8 @@ export default function App() {
right: 0, right: 0,
flex: 1, flex: 1,
}}> }}>
<Animated.FlatList
<Animated.FlatList
data={timers} data={timers}
scrollEnabled={!timerIsRunning} scrollEnabled={!timerIsRunning}
keyExtractor={item => item.toString()} keyExtractor={item => item.toString()}
@@ -586,74 +546,58 @@ export default function App() {
const clampedIndex = Math.max(0, Math.min(index, timers.length - 1)); const clampedIndex = Math.max(0, Math.min(index, timers.length - 1));
setDuration(timers[clampedIndex]); setDuration(timers[clampedIndex]);
}} }}
snapToInterval={ITEM_SIZE} snapToInterval={ITEM_SIZE}
decelerationRate="fast" decelerationRate={"fast"}
style={{flexGrow: 0}} style={{flexGrow: 0}}
contentContainerStyle={{ contentContainerStyle={{
paddingHorizontal: ITEM_SPACING paddingHorizontal: ITEM_SPACING
}} }}
renderItem={({item, index}) => { renderItem={({item, index}) => {
const timerText = item;
const inputRange = [ const inputRange = [
(index - 1) * ITEM_SIZE, (index - 1) * ITEM_SIZE,
index * ITEM_SIZE, index * ITEM_SIZE,
(index + 1) * ITEM_SIZE, (index + 1) * ITEM_SIZE,
]; ]
const normalOpacity = scrollX.interpolate({ const normalOpacity = scrollX.interpolate({
inputRange, inputRange,
outputRange: [.4, 1, .4] outputRange: [.4, 1, .4]
}); })
const timerTextOpacity = Animated.multiply(normalOpacity, inactiveTimerNumberOpacity); const opacity = Animated.multiply(normalOpacity, inactiveTimerNumberOpacity);
const scale = scrollX.interpolate({ const scale = scrollX.interpolate({
inputRange, inputRange,
outputRange: [.7, 1, .7] outputRange: [.7, 1, .7]
}); })
return <View style={{width: ITEM_SIZE, justifyContent: 'center', alignItems: 'center'}}>
<Animated.Text style={[styles.text, {
opacity,
transform: [{
scale
}]
return ( }]}>
<View style={styles.timerItem}> {timerText}
<Animated.Text style={[
styles.text,
{
opacity: timerTextOpacity,
transform: [{ scale }]
}
]}>
{item}
</Animated.Text> </Animated.Text>
</View> </View>
); }
}}
/>
</View>
);
const renderTaskDetails = () => (
<Animated.View
pointerEvents="none"
style={[
styles.taskDetails,
{
opacity: taskDetailsOpacity,
transform: [{ translateY: taskDetailsTranslateY }]
} }
]}> />
<Text style={styles.taskName}>{placeholderTask.name}</Text> </View>
<Text style={styles.taskDescription}>{placeholderTask.description}</Text> <Animated.View
</Animated.View> pointerEvents="none"
); style={[
styles.taskDetails,
return ( {
<View opacity: taskDetailsOpacity,
style={styles.container} transform: [{
onLayout={(event) => { translateY: taskDetailsTranslateY
setContainerHeight(event.nativeEvent.layout.height); }]
}}> }
<StatusBar hidden /> ]}>
{renderTimerOverlay()} <Text style={styles.taskName}>{placeholderTask.name}</Text>
{renderStartButton()} <Text style={styles.taskDescription}>{placeholderTask.description}</Text>
{renderCancelButton()} </Animated.View>
{renderCountdownOverlay()}
{renderDurationPicker()}
{renderTaskDetails()}
</View> </View>
); );
} }
@@ -677,11 +621,6 @@ const styles = StyleSheet.create({
color: colors.text, color: colors.text,
fontWeight: '900', fontWeight: '900',
}, },
timerItem: {
width: ITEM_SIZE,
justifyContent: 'center',
alignItems: 'center',
},
taskDetails: { taskDetails: {
position: 'absolute', position: 'absolute',
top: height * 0.34, top: height * 0.34,