Skip to content

Commit

Permalink
implemented accordion effect for macros change overview
Browse files Browse the repository at this point in the history
  • Loading branch information
ebbmango committed Dec 24, 2024
1 parent a74b5b0 commit 84a4ee1
Show file tree
Hide file tree
Showing 9 changed files with 493 additions and 90 deletions.
3 changes: 3 additions & 0 deletions components/Screens/Create/AnimatedBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ export default function AnimatedBar({ amount, maxAmount, color }: AnimatedBarPro

return {
height: 30,
// PROPORTION:
// Available Width is to Max Value as Animated Width is to Current Value.
// + 8 is simply so that the bar is visible from the start.
width: (availableWidth * animatedWidth.value) / animatedMax.value + 8,
backgroundColor: color,
borderBottomEndRadius: 5,
Expand Down
112 changes: 112 additions & 0 deletions components/Screens/Read/HorizontalUnitPicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import React, { useRef, useState } from 'react';
import {
FlatList,
TouchableOpacity,
StyleSheet,
View,
Text,
NativeScrollEvent,
NativeSyntheticEvent,
Pressable,
} from 'react-native';
import { Colors } from 'react-native-ui-lib';

type Unit = {
id: number;
symbol: string;
};

interface HorizontalUnitsPickerProps {
data: Unit[];
wheelWidth: number; // The visible width of this picker
wheelHeight?: number; // Let the parent specify a fixed height for the picker
}

export default function HorizontalUnitsPicker({
data,
wheelWidth,
wheelHeight = 80, // fallback to 80 if none given
}: HorizontalUnitsPickerProps) {
const [selectedIndex, setSelectedIndex] = useState<number>(0);
const flatListRef = useRef<FlatList<Unit>>(null);

// Each item takes up 1/3 of the available picker width
const ITEM_WIDTH = wheelWidth / 3;

// Called when scrolling stops. Figure out which item is centered, set it as selected.
const handleMomentumScrollEnd = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
const offsetX = e.nativeEvent.contentOffset.x;
const index = Math.round(offsetX / ITEM_WIDTH);
setSelectedIndex(index);
};

// Called when user taps an item. Scroll there, set selectedIndex.
const handlePress = (index: number) => {
setSelectedIndex(index);
flatListRef.current?.scrollToOffset({
offset: index * ITEM_WIDTH,
animated: true,
});
};

return (
<View
style={{
width: wheelWidth,
height: wheelHeight,
borderRadius: 20,
overflow: 'hidden', // ensures children respect parent’s border radius
backgroundColor: 'white',
}}>
<FlatList
ref={flatListRef}
data={data}
horizontal
keyExtractor={(item) => String(item.id)}
// Makes the FlatList fill the parent container's height
style={{ flex: 1 }}
showsHorizontalScrollIndicator={false}
getItemLayout={(_, index) => ({
length: ITEM_WIDTH,
offset: ITEM_WIDTH * index,
index,
})}
// Adds symmetrical horizontal padding so items can center
contentContainerStyle={{
paddingHorizontal: (wheelWidth - ITEM_WIDTH) / 2,
}}
snapToInterval={ITEM_WIDTH}
snapToAlignment="start"
decelerationRate="fast"
onMomentumScrollEnd={handleMomentumScrollEnd}
renderItem={({ item, index }) => {
const isSelected = index === selectedIndex;
return (
<Pressable
style={[styles.itemContainer, { width: ITEM_WIDTH }]}
onPress={() => handlePress(index)}>
<Text style={[styles.itemText, isSelected && styles.itemTextSelected]}>
{item.symbol}
</Text>
</Pressable>
);
}}
/>
</View>
);
}

const styles = StyleSheet.create({
itemContainer: {
justifyContent: 'center',
alignItems: 'center',
},
itemText: {
color: Colors.grey40,
fontSize: 18,
},
itemTextSelected: {
fontSize: 18,
color: Colors.violet30,
},
});
76 changes: 76 additions & 0 deletions components/Screens/Read/KcalsTransition.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// KcalsTransition.tsx
import React, { useEffect } from 'react';
import { LayoutChangeEvent, StyleSheet } from 'react-native';
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
Easing,
} from 'react-native-reanimated';
import { Text, View } from 'react-native-ui-lib';
import IconSVG from 'components/Shared/icons/IconSVG';
import { useColors } from 'context/ColorContext';

type KcalsTransitionProps = {
current: number;
after?: number;
expanded: boolean;
expandedHeight: number; // Added prop for expanded height
onLayout?: (event: LayoutChangeEvent) => void;
};

export default function KcalsTransition({
current,
after,
expanded,
expandedHeight,
onLayout,
}: KcalsTransitionProps) {
const colors = useColors();
const numbersLength = current.toString().length + (after?.toString().length || 0);
const textStyle = { fontSize: numbersLength > 12 ? 14 : 16 };

// Shared value for height
const height = useSharedValue(80); // Contracted height

useEffect(() => {
height.value = withTiming(expanded ? expandedHeight : 80, {
duration: 400,
easing: Easing.inOut(Easing.ease),
});
}, [expanded, expandedHeight, height]);

// Animated style
const animatedStyle = useAnimatedStyle(() => ({
height: height.value,
}));

return (
<Animated.View
onLayout={onLayout}
style={[styles.flex, { backgroundColor: colors.get('kcals') }, animatedStyle]}>
<IconSVG color="white" name="ball-pile-solid" width={20} />
<Text white style={textStyle}>
{current} {!expanded && 'kcal'}
</Text>
{expanded && (
<>
<IconSVG color={'white'} name="arrow-right-solid" width={16} />
<Text white style={textStyle}>
{after}
</Text>
</>
)}
</Animated.View>
);
}

const styles = StyleSheet.create({
flex: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-around',
paddingHorizontal: 8,
overflow: 'hidden', // Ensure content doesn't overflow during animation
},
});
41 changes: 41 additions & 0 deletions components/Screens/Read/MacrosAccordion.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React, { PropsWithChildren, useEffect } from 'react';
import { ViewStyle } from 'react-native';
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
Easing,
} from 'react-native-reanimated';

interface MacrosAccordionProps extends PropsWithChildren {
expanded: boolean;
leftoverSpace: number; // height the Accordion should animate to
}

/**
* This animated container starts at height=0 (collapsed)
* and expands to leftoverSpace when 'expanded' is true.
*/
export default function MacrosAccordion({
expanded,
leftoverSpace,
children,
}: MacrosAccordionProps) {
const heightValue = useSharedValue(0);

// Animate from 0 → leftoverSpace or leftoverSpace → 0
useEffect(() => {
heightValue.value = withTiming(expanded ? leftoverSpace : 0, {
duration: 400,
easing: Easing.inOut(Easing.ease),
});
}, [expanded, leftoverSpace]);

// Animated style: we update 'height' and optionally 'opacity'
const containerStyle = useAnimatedStyle<ViewStyle>(() => ({
height: heightValue.value,
overflow: 'hidden',
}));

return <Animated.View style={containerStyle}>{children}</Animated.View>;
}
51 changes: 51 additions & 0 deletions components/Screens/Read/MacrosTransition.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import IconSVG from 'components/Shared/icons/IconSVG';
import { useColors } from 'context/ColorContext';
import { StyleSheet } from 'react-native';
import { Text, View } from 'react-native-ui-lib';

type MacrosTransitionProps = {
current: number;
after?: number;
macro: 'carbs' | 'protein' | 'fat';
};

export default function MacrosTransition({ current, after, macro }: MacrosTransitionProps) {
const colors = useColors();

const numbersLength = current.toString().length + (after?.toString().length || 0);
const textStyle = { fontSize: numbersLength > 12 ? 14 : 16 };

return (
<View style={[styles.flex, { backgroundColor: colors.get(macro) }]}>
<IconSVG color="white" name={determineIcon(macro)} width={20} />
<Text white style={textStyle}>
{current}
</Text>
<IconSVG color={'white'} name="arrow-right-solid" width={16} />
<Text white style={textStyle}>
{after}
</Text>
</View>
);
}

function determineIcon(macro: 'carbs' | 'protein' | 'fat') {
switch (macro) {
case 'fat':
return 'bacon-solid';
case 'carbs':
return 'wheat-solid';
case 'protein':
return 'meat-solid';
}
}

const styles = StyleSheet.create({
flex: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-around',
paddingHorizontal: 8,
},
});
Loading

0 comments on commit 84a4ee1

Please sign in to comment.