Create animated accordion component for reuse in the app

This commit is contained in:
Arnaud Vergnet 2020-04-19 21:44:58 +02:00
parent 581ea516ae
commit 7f24eb77ac
5 changed files with 127 additions and 105 deletions

View file

@ -0,0 +1,87 @@
// @flow
import * as React from 'react';
import {View} from "react-native";
import {List, withTheme} from 'react-native-paper';
import Collapsible from "react-native-collapsible";
import * as Animatable from "react-native-animatable";
import type {CustomTheme} from "../../managers/ThemeManager";
type Props = {
theme: CustomTheme,
title: string,
subtitle?: string,
left?: (props: { [keys: string]: any }) => React.Node,
startOpen: boolean,
keepOpen: boolean,
unmountWhenCollapsed: boolean,
children?: React.Node,
}
type State = {
expanded: boolean
}
const AnimatedListIcon = Animatable.createAnimatableComponent(List.Icon);
class AnimatedAccordion extends React.PureComponent<Props, State> {
static defaultProps = {
startOpen: false,
keepOpen: false,
unmountWhenCollapsed: false,
}
chevronRef: { current: null | AnimatedListIcon };
state = {
expanded: false,
}
constructor(props) {
super(props);
this.chevronRef = React.createRef();
}
componentDidMount() {
if (this.props.startOpen)
this.toggleAccordion();
}
toggleAccordion = () => {
if (!this.props.keepOpen) {
if (this.chevronRef.current != null)
this.chevronRef.current.transitionTo({rotate: this.state.expanded ? '0deg' : '180deg'});
this.setState({expanded: !this.state.expanded})
}
};
render() {
const colors = this.props.theme.colors;
return (
<View>
<List.Item
{...this.props}
title={this.props.title}
subtitle={this.props.subtitle}
titleStyle={this.state.expanded ? {color: colors.primary} : undefined}
onPress={this.toggleAccordion}
right={(props) => <AnimatedListIcon
ref={this.chevronRef}
{...props}
icon={"chevron-down"}
color={this.state.expanded ? colors.primary : undefined}
useNativeDriver
/>}
left={this.props.left}
/>
<Collapsible collapsed={!this.props.keepOpen && !this.state.expanded}>
{!this.props.unmountWhenCollapsed || (this.props.unmountWhenCollapsed && this.state.expanded)
? this.props.children
: null}
</Collapsible>
</View>
);
}
}
export default withTheme(AnimatedAccordion);

View file

@ -4,21 +4,15 @@ import * as React from 'react';
import {Card, List, Text, withTheme} from 'react-native-paper'; import {Card, List, Text, withTheme} from 'react-native-paper';
import {StyleSheet, View} from "react-native"; import {StyleSheet, View} from "react-native";
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import AnimatedAccordion from "../../Animations/AnimatedAccordion";
type Props = { type Props = {
categoryRender: Function, categoryRender: Function,
categories: Array<Object>, categories: Array<Object>,
} }
type State = { class ClubListHeader extends React.Component<Props> {
expanded: boolean,
}
class ClubListHeader extends React.Component<Props, State> {
state = {
expanded: true
};
colors: Object; colors: Object;
@ -35,22 +29,19 @@ class ClubListHeader extends React.Component<Props, State> {
return final; return final;
} }
onPress = () => this.setState({expanded: !this.state.expanded});
render() { render() {
return ( return (
<Card style={styles.card}> <Card style={styles.card}>
<List.Accordion <AnimatedAccordion
title={i18n.t("clubs.categories")} title={i18n.t("clubs.categories")}
left={props => <List.Icon {...props} icon="star"/>} left={props => <List.Icon {...props} icon="star"/>}
expanded={this.state.expanded} startOpen={true}
onPress={this.onPress}
> >
<Text style={styles.text}>{i18n.t("clubs.categoriesFilterMessage")}</Text> <Text style={styles.text}>{i18n.t("clubs.categoriesFilterMessage")}</Text>
<View style={styles.chipContainer}> <View style={styles.chipContainer}>
{this.getCategoriesRender()} {this.getCategoriesRender()}
</View> </View>
</List.Accordion> </AnimatedAccordion>
</Card> </Card>
); );
} }

View file

@ -4,9 +4,9 @@ import * as React from 'react';
import {List, withTheme} from 'react-native-paper'; import {List, withTheme} from 'react-native-paper';
import {FlatList, View} from "react-native"; import {FlatList, View} from "react-native";
import {stringMatchQuery} from "../../../utils/Search"; import {stringMatchQuery} from "../../../utils/Search";
import Collapsible from "react-native-collapsible";
import * as Animatable from "react-native-animatable"; import * as Animatable from "react-native-animatable";
import GroupListItem from "./GroupListItem"; import GroupListItem from "./GroupListItem";
import AnimatedAccordion from "../../Animations/AnimatedAccordion";
type Props = { type Props = {
item: Object, item: Object,
@ -47,11 +47,6 @@ class GroupListAccordion extends React.Component<Props, State> {
|| (nextProps.item.content.length !== this.props.item.content.length); || (nextProps.item.content.length !== this.props.item.content.length);
} }
onPress = () => {
this.chevronRef.current.transitionTo({rotate: this.state.expanded ? '0deg' : '180deg'});
this.setState({expanded: !this.state.expanded})
};
keyExtractor = (item: Object) => item.id.toString(); keyExtractor = (item: Object) => item.id.toString();
renderItem = ({item}: Object) => { renderItem = ({item}: Object) => {
@ -73,20 +68,14 @@ class GroupListAccordion extends React.Component<Props, State> {
render() { render() {
const item = this.props.item; const item = this.props.item;
const accordionColor = this.state.expanded
? this.props.theme.colors.primary
: this.props.theme.colors.text;
// console.log(item.id);
return ( return (
<View> <View>
<List.Item <AnimatedAccordion
title={item.name} title={item.name}
onPress={this.onPress}
style={{ style={{
height: this.props.height, height: this.props.height,
justifyContent: 'center', justifyContent: 'center',
}} }}
titleStyle={{color: accordionColor}}
left={props => left={props =>
item.id === "0" item.id === "0"
? <List.Icon ? <List.Icon
@ -95,35 +84,19 @@ class GroupListAccordion extends React.Component<Props, State> {
color={this.props.theme.colors.tetrisScore} color={this.props.theme.colors.tetrisScore}
/> />
: null} : null}
right={(props) => <AnimatedListIcon unmountWhenCollapsed={true}// Only render list if expanded for increased performance
ref={this.chevronRef}
{...props}
icon={"chevron-down"}
color={this.state.expanded
? this.props.theme.colors.primary
: props.color
}
useNativeDriver
/>}
/>
<Collapsible
collapsed={!this.state.expanded}
ease={"easeInOut"}
> >
{this.state.expanded // Only render list if expanded for increased performance <FlatList
? <FlatList data={item.content}
data={item.content} extraData={this.props.currentSearchString}
extraData={this.props.currentSearchString} renderItem={this.renderItem}
renderItem={this.renderItem} keyExtractor={this.keyExtractor}
keyExtractor={this.keyExtractor} listKey={item.id}
listKey={item.id} // Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration
// Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration getItemLayout={this.itemLayout} // Broken with search
getItemLayout={this.itemLayout} // Broken with search removeClippedSubviews={true}
removeClippedSubviews={true} />
/> </AnimatedAccordion>
: null}
</Collapsible>
</View> </View>
); );
} }

View file

@ -1,11 +1,10 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {FlatList, View} from "react-native"; import {FlatList} from "react-native";
import {Drawer, List, withTheme} from 'react-native-paper'; import {Drawer, withTheme} from 'react-native-paper';
import {Linking} from "expo"; import {Linking} from "expo";
import Collapsible from "react-native-collapsible"; import AnimatedAccordion from "../Animations/AnimatedAccordion";
import * as Animatable from "react-native-animatable";
type Props = { type Props = {
navigation: Object, navigation: Object,
@ -17,28 +16,17 @@ type Props = {
listData: Array<Object>, listData: Array<Object>,
} }
type State = {
expanded: boolean
}
const AnimatedListIcon = Animatable.createAnimatableComponent(List.Icon);
const LIST_ITEM_HEIGHT = 48; const LIST_ITEM_HEIGHT = 48;
class SideBarSection extends React.PureComponent<Props, State> { class SideBarSection extends React.PureComponent<Props> {
state = {
expanded: this.props.startOpen,
};
colors: Object; colors: Object;
shouldExpand: boolean; accordionRef: {current: null | AnimatedAccordion};
chevronRef: Object;
constructor(props) { constructor(props) {
super(props); super(props);
this.colors = props.theme.colors; this.colors = props.theme.colors;
this.chevronRef = React.createRef(); this.accordionRef = React.createRef();
} }
/** /**
@ -50,7 +38,7 @@ class SideBarSection extends React.PureComponent<Props, State> {
shouldExpandList() { shouldExpandList() {
for (let i = 0; i < this.props.listData.length; i++) { for (let i = 0; i < this.props.listData.length; i++) {
if (this.props.listData[i].route === this.props.activeRoute) { if (this.props.listData[i].route === this.props.activeRoute) {
return this.state.expanded === false; return true;
} }
} }
return false; return false;
@ -109,13 +97,6 @@ class SideBarSection extends React.PureComponent<Props, State> {
); );
}; };
toggleAccordion = () => {
if ((!this.state.expanded && this.shouldExpand) || !this.shouldExpand) {
this.chevronRef.current.transitionTo({ rotate: this.state.expanded ? '0deg' : '180deg' });
this.setState({expanded: !this.state.expanded})
}
};
shouldRenderAccordion() { shouldRenderAccordion() {
let itemsToRender = 0; let itemsToRender = 0;
for (let i = 0; i < this.props.listData.length; i++) { for (let i = 0; i < this.props.listData.length; i++) {
@ -144,26 +125,13 @@ class SideBarSection extends React.PureComponent<Props, State> {
render() { render() {
if (this.shouldRenderAccordion()) { if (this.shouldRenderAccordion()) {
this.shouldExpand = this.shouldExpandList();
if (this.shouldExpand)
this.toggleAccordion();
return ( return (
<View> <AnimatedAccordion
<List.Item title={this.props.sectionName}
title={this.props.sectionName} keepOpen={this.shouldExpandList()}
// expanded={this.state.expanded} >
onPress={this.toggleAccordion} {this.getFlatList()}
right={(props) => <AnimatedListIcon </AnimatedAccordion>
ref={this.chevronRef}
{...props}
icon={"chevron-down"}
useNativeDriver
/>}
/>
<Collapsible collapsed={!this.state.expanded}>
{this.getFlatList()}
</Collapsible>
</View>
); );
} else } else
return this.getFlatList(); return this.getFlatList();

View file

@ -8,6 +8,7 @@ import AsyncStorageManager from "../../managers/AsyncStorageManager";
import {setMachineReminderNotificationTime} from "../../utils/Notifications"; import {setMachineReminderNotificationTime} from "../../utils/Notifications";
import {Card, List, Switch, ToggleButton} from 'react-native-paper'; import {Card, List, Switch, ToggleButton} from 'react-native-paper';
import {Appearance} from "react-native-appearance"; import {Appearance} from "react-native-appearance";
import AnimatedAccordion from "../../components/Animations/AnimatedAccordion";
type Props = { type Props = {
navigation: Object, navigation: Object,
@ -89,6 +90,7 @@ export default class SettingsScreen extends React.Component<Props, State> {
<ToggleButton.Row <ToggleButton.Row
onValueChange={this.onProxiwashNotifPickerValueChange} onValueChange={this.onProxiwashNotifPickerValueChange}
value={this.state.proxiwashNotifPickerSelected} value={this.state.proxiwashNotifPickerSelected}
style={{marginLeft: 'auto', marginRight: 'auto'}}
> >
<ToggleButton icon="close" value="never"/> <ToggleButton icon="close" value="never"/>
<ToggleButton icon="numeric-2" value="2"/> <ToggleButton icon="numeric-2" value="2"/>
@ -107,6 +109,7 @@ export default class SettingsScreen extends React.Component<Props, State> {
<ToggleButton.Row <ToggleButton.Row
onValueChange={this.onStartScreenPickerValueChange} onValueChange={this.onStartScreenPickerValueChange}
value={this.state.startScreenPickerSelected} value={this.state.startScreenPickerSelected}
style={{marginLeft: 'auto', marginRight: 'auto'}}
> >
<ToggleButton icon="shopping" value="proximo"/> <ToggleButton icon="shopping" value="proximo"/>
<ToggleButton icon="calendar-range" value="planning"/> <ToggleButton icon="calendar-range" value="planning"/>
@ -188,25 +191,25 @@ export default class SettingsScreen extends React.Component<Props, State> {
this.state.nightMode this.state.nightMode
) : null ) : null
} }
<List.Accordion <AnimatedAccordion
title={i18n.t('settingsScreen.startScreen')} title={i18n.t('settingsScreen.startScreen')}
description={i18n.t('settingsScreen.startScreenSub')} subtitle={i18n.t('settingsScreen.startScreenSub')}
left={props => <List.Icon {...props} icon="power"/>} left={props => <List.Icon {...props} icon="power"/>}
> >
{this.getStartScreenPicker()} {this.getStartScreenPicker()}
</List.Accordion> </AnimatedAccordion>
</List.Section> </List.Section>
</Card> </Card>
<Card style={{margin: 5}}> <Card style={{margin: 5}}>
<Card.Title title="Proxiwash"/> <Card.Title title="Proxiwash"/>
<List.Section> <List.Section>
<List.Accordion <AnimatedAccordion
title={i18n.t('settingsScreen.proxiwashNotifReminder')} title={i18n.t('settingsScreen.proxiwashNotifReminder')}
description={i18n.t('settingsScreen.proxiwashNotifReminderSub')} description={i18n.t('settingsScreen.proxiwashNotifReminderSub')}
left={props => <List.Icon {...props} icon="washing-machine"/>} left={props => <List.Icon {...props} icon="washing-machine"/>}
> >
{this.getProxiwashNotifPicker()} {this.getProxiwashNotifPicker()}
</List.Accordion> </AnimatedAccordion>
</List.Section> </List.Section>
</Card> </Card>
</ScrollView> </ScrollView>