Skip to content

Commit 0b212a1

Browse files
committed
feat: animate List.Accordion expand and collapse
1 parent 802309e commit 0b212a1

2 files changed

Lines changed: 349 additions & 199 deletions

File tree

‎src/components/List/ListAccordion.tsx‎

Lines changed: 90 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,25 @@ import type {
1212
ViewStyle,
1313
} from 'react-native';
1414

15+
import Animated, {
16+
Easing,
17+
ReduceMotion,
18+
useAnimatedStyle,
19+
useSharedValue,
20+
withTiming,
21+
} from 'react-native-reanimated';
22+
import type {
23+
EntryAnimationsValues,
24+
ExitAnimationsValues,
25+
} from 'react-native-reanimated';
26+
1527
import { ListAccordionGroupContext } from './ListAccordionGroup';
1628
import { ListTokens } from './tokens';
1729
import type { ListChildProps, Style } from './utils';
1830
import { getLeftStyles } from './utils';
1931
import { useLocale } from '../../core/locale';
2032
import { useInternalTheme } from '../../core/theming';
33+
import { useReduceMotion } from '../../theme/accessibility/ReduceMotionContext';
2134
import type { ThemeProp } from '../../types';
2235
import MaterialCommunityIcon from '../MaterialCommunityIcon';
2336
import TouchableRipple from '../TouchableRipple/TouchableRipple';
@@ -249,6 +262,62 @@ const ListAccordion = ({
249262
? groupContext.expandedId === id
250263
: expandedInternal;
251264

265+
const reduceMotion = useReduceMotion();
266+
const reanimatedReduceMotion = reduceMotion
267+
? ReduceMotion.Always
268+
: ReduceMotion.Never;
269+
270+
const timingConfig = React.useMemo(
271+
() => ({
272+
duration: theme.motion.duration.medium2,
273+
easing: Easing.bezier(...theme.motion.easing.emphasized),
274+
reduceMotion: reanimatedReduceMotion,
275+
}),
276+
[
277+
theme.motion.duration.medium2,
278+
theme.motion.easing.emphasized,
279+
reanimatedReduceMotion,
280+
]
281+
);
282+
283+
const chevronProgress = useSharedValue(isExpanded ? 1 : 0);
284+
285+
React.useEffect(() => {
286+
chevronProgress.value = withTiming(isExpanded ? 1 : 0, timingConfig);
287+
}, [isExpanded, chevronProgress, timingConfig]);
288+
289+
const chevronStyle = useAnimatedStyle(() => ({
290+
transform: [{ rotate: `${chevronProgress.value * 180}deg` }],
291+
}));
292+
293+
const expandAnimation = React.useCallback(
294+
(values: EntryAnimationsValues) => {
295+
'worklet';
296+
return {
297+
initialValues: { height: 0, opacity: 0 },
298+
animations: {
299+
height: withTiming(values.targetHeight, timingConfig),
300+
opacity: withTiming(1, timingConfig),
301+
},
302+
};
303+
},
304+
[timingConfig]
305+
);
306+
307+
const collapseAnimation = React.useCallback(
308+
(values: ExitAnimationsValues) => {
309+
'worklet';
310+
return {
311+
initialValues: { height: values.currentHeight, opacity: 1 },
312+
animations: {
313+
height: withTiming(0, timingConfig),
314+
opacity: withTiming(0, timingConfig),
315+
},
316+
};
317+
},
318+
[timingConfig]
319+
);
320+
252321
const selectedContentColor = theme.colors[ListTokens.selectedContentColor];
253322
const titleTextColor = selected
254323
? selectedContentColor
@@ -342,20 +411,27 @@ const ListAccordion = ({
342411
isExpanded: isExpanded,
343412
})
344413
) : (
345-
<MaterialCommunityIcon
346-
name={isExpanded ? 'chevron-up' : 'chevron-down'}
347-
color={trailingIconColor}
348-
size={24}
349-
direction={direction}
350-
/>
414+
<Animated.View style={chevronStyle}>
415+
<MaterialCommunityIcon
416+
name="chevron-down"
417+
color={trailingIconColor}
418+
size={24}
419+
direction={direction}
420+
/>
421+
</Animated.View>
351422
)}
352423
</View>
353424
</View>
354425
</TouchableRipple>
355426
</View>
356427

357-
{isExpanded
358-
? React.Children.map(children, (child) => {
428+
{isExpanded ? (
429+
<Animated.View
430+
entering={expandAnimation}
431+
exiting={collapseAnimation}
432+
style={styles.expandedContent}
433+
>
434+
{React.Children.map(children, (child) => {
359435
if (
360436
left &&
361437
React.isValidElement<ListChildProps>(child) &&
@@ -369,8 +445,9 @@ const ListAccordion = ({
369445
}
370446

371447
return child;
372-
})
373-
: null}
448+
})}
449+
</Animated.View>
450+
) : null}
374451
</View>
375452
);
376453
};
@@ -403,6 +480,9 @@ const styles = StyleSheet.create({
403480
child: {
404481
paddingLeft: 40,
405482
},
483+
expandedContent: {
484+
overflow: 'hidden',
485+
},
406486
content: {
407487
flex: 1,
408488
justifyContent: 'center',

0 commit comments

Comments
 (0)