Create animated accordion component for reuse in the app
This commit is contained in:
parent
581ea516ae
commit
7f24eb77ac
5 changed files with 127 additions and 105 deletions
87
src/components/Animations/AnimatedAccordion.js
Normal file
87
src/components/Animations/AnimatedAccordion.js
Normal 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);
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue