Fixed bug where timer would count space below nav bar as 'usable' space

This commit is contained in:
Chris Sanden
2026-04-22 19:12:34 +02:00
parent 145a04232b
commit b7d62637e6

View File

@@ -4,6 +4,7 @@ import {
Dimensions, Dimensions,
StatusBar, StatusBar,
StyleSheet, StyleSheet,
Text,
TouchableOpacity, TouchableOpacity,
View View
} from 'react-native'; } from 'react-native';
@@ -18,6 +19,10 @@ const colors = {
const timers = [...Array(13).keys()].map((i) => (i === 0 ? 1 : i * 5)); const timers = [...Array(13).keys()].map((i) => (i === 0 ? 1 : i * 5));
const ITEM_SIZE = width * 0.38; const ITEM_SIZE = width * 0.38;
const ITEM_SPACING = (width - ITEM_SIZE) / 2; const ITEM_SPACING = (width - ITEM_SIZE) / 2;
const placeholderTask = {
name: 'Read chapter 4',
description: 'Focus on the summary questions and write down anything unclear.',
};
/* /*
Har bare skrevet timeren som en egen tab til å begynne med. Har bare skrevet timeren som en egen tab til å begynne med.
@@ -26,136 +31,212 @@ som viser TaskName og Description der tallene står nå
Kanskje en animert figur hvis vi får tid Kanskje en animert figur hvis vi får tid
*/ */
export default function App() { export default function App() {
const scrollX = React.useRef(new Animated.Value(0)).current; const [containerHeight, setContainerHeight] = React.useState(0)
const [duration, setDuration] = React.useState(timers[0]) const scrollX = React.useRef(new Animated.Value(0)).current;
const timerAnimation = React.useRef(new Animated.Value(height)).current const [duration, setDuration] = React.useState(timers[0])
const buttonAnimation = React.useRef(new Animated.Value(0)).current const [isRunning, setIsRunning] = React.useState(false)
const animation = React.useCallback(() => { const timerAnimation = React.useRef(new Animated.Value(0)).current
Animated.sequence([ const buttonAnimation = React.useRef(new Animated.Value(0)).current
const taskDetailsAnimation = React.useRef(new Animated.Value(0)).current
const animation = React.useCallback(() => {
if (isRunning) {
return;
}
setIsRunning(true);
taskDetailsAnimation.setValue(0);
Animated.sequence([
Animated.parallel([
Animated.timing(buttonAnimation, { Animated.timing(buttonAnimation, {
toValue: 1, toValue: 1,
duration: 300, duration: 300,
useNativeDriver: true useNativeDriver: true
}), }),
Animated.timing(timerAnimation, { Animated.timing(taskDetailsAnimation, {
toValue: 0, toValue: 1,
duration: 300, duration: 500,
useNativeDriver: true useNativeDriver: true
}), }),
Animated.timing(timerAnimation, { ]),
toValue: height, Animated.timing(timerAnimation, {
duration: duration * 1000, toValue: 0,
useNativeDriver: true duration: 300,
}), useNativeDriver: true
]) .start(() => { }),
Animated.timing(timerAnimation, {
toValue: containerHeight,
duration: duration * 1000,
useNativeDriver: true
}),
]) .start(({ finished }) => {
if (!finished) {
setIsRunning(false);
return;
}
Animated.parallel([
Animated.timing(buttonAnimation, { Animated.timing(buttonAnimation, {
toValue: 0, toValue: 0,
duration: 300, duration: 300,
useNativeDriver: true useNativeDriver: true
}).start() }),
Animated.timing(taskDetailsAnimation, {
toValue: 0,
duration: 300,
useNativeDriver: true
}),
]).start(() => {
setIsRunning(false);
}) })
}, [duration])
const opacity = buttonAnimation.interpolate({
inputRange: [0, 1],
outputRange: [1, 0]
}) })
const translateY = buttonAnimation.interpolate({ }, [buttonAnimation, duration, isRunning, taskDetailsAnimation, timerAnimation])
inputRange: [0, 1],
outputRange: [0, 200] React.useEffect(() => {
if (containerHeight > 0) {
timerAnimation.setValue(containerHeight);
}
}) })
return (
<View style={styles.container}>
<StatusBar hidden />
<Animated.View
style={[StyleSheet.absoluteFillObject, {
height,
width,
backgroundColor: colors.red,
transform: [{
translateY: timerAnimation
}]
}]}
/>
<Animated.View
style={[
StyleSheet.absoluteFillObject,
{
justifyContent: 'flex-end',
alignItems: 'center',
paddingBottom: 100,
opacity,
transform: [{
translateY
}]
},
]}>
<TouchableOpacity
onPress={animation}>
<View
style={styles.roundButton}
/>
</TouchableOpacity>
</Animated.View>
<View
style={{
position: 'absolute',
top: height / 3,
left: 0,
right: 0,
flex: 1,
}}>
<Animated.FlatList
data={timers}
keyExtractor={item => item.toString()}
horizontal
bounces={false}
onScroll={Animated.event(
[{nativeEvent: {contentOffset: {x: scrollX}}}],
{ useNativeDriver: true}
)}
showsHorizontalScrollIndicator={false}
onMomentumScrollEnd={ev => {
const index = Math.round(ev.nativeEvent.contentOffset.x / ITEM_SIZE)
setDuration(timers[index]);
}}
snapToInterval={ITEM_SIZE}
decelerationRate={"fast"}
style={{flexGrow: 0}}
contentContainerStyle={{
paddingHorizontal: ITEM_SPACING
}}
renderItem={({item, index}) => {
const inputRange = [
(index - 1) * ITEM_SIZE,
index * ITEM_SIZE,
(index + 1) * ITEM_SIZE,
]
const opacity = scrollX.interpolate({ const opacity = buttonAnimation.interpolate({
inputRange, inputRange: [0, 1],
outputRange: [.4, 1, .4] outputRange: [1, 0]
}) })
const scale = scrollX.interpolate({ const translateY = buttonAnimation.interpolate({
inputRange, inputRange: [0, 1],
outputRange: [.7, 1, .7] outputRange: [0, 200]
}) })
return <View style={{width: ITEM_SIZE, justifyContent: 'center', alignItems: 'center'}}> const inactiveTimerNumberOpacity = buttonAnimation.interpolate({
<Animated.Text style={[styles.text, { inputRange: [0, 1],
opacity, outputRange: [1, 0]
transform: [{ })
scale const taskDetailsOpacity = taskDetailsAnimation.interpolate({
}] inputRange: [0, 1],
outputRange: [0, 1]
})
const taskDetailsTranslateY = taskDetailsAnimation.interpolate({
inputRange: [0, 1],
outputRange: [20, 0]
})
return (
<View style={styles.container}
onLayout={(event) => {
setContainerHeight(event.nativeEvent.layout.height);
}}>
<StatusBar hidden />
<Animated.View
style={[StyleSheet.absoluteFillObject, {
height,
width,
backgroundColor: colors.red,
transform: [{
translateY: timerAnimation
}]
}]}
/>
<Animated.View
style={[
StyleSheet.absoluteFillObject,
{
justifyContent: 'flex-end',
alignItems: 'center',
paddingBottom: 100,
opacity,
transform: [{
translateY
}]
},
]}>
<TouchableOpacity
disabled={isRunning}
onPress={animation}>
<View
style={styles.roundButton}
/>
</TouchableOpacity>
</Animated.View>
<View
style={{
position: 'absolute',
top: containerHeight / 3,
left: 0,
right: 0,
flex: 1,
}}>
<Animated.FlatList
data={timers}
keyExtractor={item => item.toString()}
horizontal
bounces={false}
onScroll={Animated.event(
[{nativeEvent: {contentOffset: {x: scrollX}}}],
{ useNativeDriver: true}
)}
showsHorizontalScrollIndicator={false}
onMomentumScrollEnd={ev => {
const index = Math.round(ev.nativeEvent.contentOffset.x / ITEM_SIZE)
setDuration(timers[index]);
}}
snapToInterval={ITEM_SIZE}
decelerationRate={"fast"}
style={{flexGrow: 0}}
contentContainerStyle={{
paddingHorizontal: ITEM_SPACING
}}
renderItem={({item, index}) => {
const inputRange = [
(index - 1) * ITEM_SIZE,
index * ITEM_SIZE,
(index + 1) * ITEM_SIZE,
]
}]}> const normalOpacity = scrollX.interpolate({
{item} inputRange,
</Animated.Text> outputRange: [.4, 1, .4]
</View> })
} const selectedOpacity = scrollX.interpolate({
inputRange,
outputRange: [0, 1, 0],
extrapolate: 'clamp'
})
const opacity = Animated.add(
Animated.multiply(normalOpacity, inactiveTimerNumberOpacity),
Animated.multiply(selectedOpacity, buttonAnimation)
)
const scale = scrollX.interpolate({
inputRange,
outputRange: [.7, 1, .7]
})
return <View style={{width: ITEM_SIZE, justifyContent: 'center', alignItems: 'center'}}>
<Animated.Text style={[styles.text, {
opacity,
transform: [{
scale
}]
}]}>
{item}
</Animated.Text>
</View>
} }
/> }
</View> />
</View>
<Animated.View
pointerEvents="none"
style={[
styles.taskDetails,
{
opacity: taskDetailsOpacity,
transform: [{
translateY: taskDetailsTranslateY
}]
}
]}>
<Text style={styles.taskName}>{placeholderTask.name}</Text>
<Text style={styles.taskDescription}>{placeholderTask.description}</Text>
</Animated.View>
</View> </View>
); );
} }
@@ -176,5 +257,25 @@ const styles = StyleSheet.create({
fontFamily: 'Menlo', fontFamily: 'Menlo',
color: colors.text, color: colors.text,
fontWeight: '900', fontWeight: '900',
},
taskDetails: {
position: 'absolute',
top: height * 0.56,
left: 32,
right: 32,
alignItems: 'center',
},
taskName: {
color: colors.text,
fontSize: 24,
fontWeight: '800',
textAlign: 'center',
},
taskDescription: {
color: colors.text,
fontSize: 16,
lineHeight: 22,
marginTop: 10,
textAlign: 'center',
} }
}); });