Improved accordion performance

This commit is contained in:
Arnaud Vergnet 2020-04-28 20:18:52 +02:00
parent 1e0cc867b8
commit 070d6beb83
5 changed files with 72 additions and 65 deletions

View file

@ -12,8 +12,7 @@ type Props = {
title: string, title: string,
subtitle?: string, subtitle?: string,
left?: (props: { [keys: string]: any }) => React.Node, left?: (props: { [keys: string]: any }) => React.Node,
startOpen: boolean, opened: boolean,
keepOpen: boolean,
unmountWhenCollapsed: boolean, unmountWhenCollapsed: boolean,
children?: React.Node, children?: React.Node,
} }
@ -24,36 +23,51 @@ type State = {
const AnimatedListIcon = Animatable.createAnimatableComponent(List.Icon); const AnimatedListIcon = Animatable.createAnimatableComponent(List.Icon);
class AnimatedAccordion extends React.PureComponent<Props, State> { class AnimatedAccordion extends React.Component<Props, State> {
static defaultProps = { static defaultProps = {
startOpen: false, opened: false,
keepOpen: false,
unmountWhenCollapsed: false, unmountWhenCollapsed: false,
} }
chevronRef: { current: null | AnimatedListIcon }; chevronRef: { current: null | AnimatedListIcon };
chevronIcon: string;
animStart: string;
animEnd: string;
state = { state = {
expanded: false, expanded: this.props.opened,
} }
constructor(props) { constructor(props) {
super(props); super(props);
this.chevronRef = React.createRef(); this.chevronRef = React.createRef();
this.setupChevron();
} }
componentDidMount() { setupChevron() {
if (this.props.startOpen) if (this.state.expanded) {
this.toggleAccordion(); this.chevronIcon = "chevron-up";
this.animStart = "180deg";
this.animEnd = "0deg";
} else {
this.chevronIcon = "chevron-down";
this.animStart = "0deg";
this.animEnd = "180deg";
}
} }
toggleAccordion = () => { toggleAccordion = () => {
if (!this.props.keepOpen) { if (this.chevronRef.current != null)
if (this.chevronRef.current != null) this.chevronRef.current.transitionTo({rotate: this.state.expanded ? this.animStart : this.animEnd});
this.chevronRef.current.transitionTo({rotate: this.state.expanded ? '0deg' : '180deg'}); this.setState({expanded: !this.state.expanded})
this.setState({expanded: !this.state.expanded})
}
}; };
shouldComponentUpdate(nextProps: Props) {
this.state.expanded = nextProps.opened;
this.setupChevron();
return true;
}
render() { render() {
const colors = this.props.theme.colors; const colors = this.props.theme.colors;
return ( return (
@ -67,13 +81,13 @@ class AnimatedAccordion extends React.PureComponent<Props, State> {
right={(props) => <AnimatedListIcon right={(props) => <AnimatedListIcon
ref={this.chevronRef} ref={this.chevronRef}
{...props} {...props}
icon={"chevron-down"} icon={this.chevronIcon}
color={this.state.expanded ? colors.primary : undefined} color={this.state.expanded ? colors.primary : undefined}
useNativeDriver useNativeDriver
/>} />}
left={this.props.left} left={this.props.left}
/> />
<Collapsible collapsed={!this.props.keepOpen && !this.state.expanded}> <Collapsible collapsed={!this.state.expanded}>
{!this.props.unmountWhenCollapsed || (this.props.unmountWhenCollapsed && this.state.expanded) {!this.props.unmountWhenCollapsed || (this.props.unmountWhenCollapsed && this.state.expanded)
? this.props.children ? this.props.children
: null} : null}

View file

@ -1,22 +1,43 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {Card, List, Text} from 'react-native-paper'; import {Card, Chip, List, Text} 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"; import AnimatedAccordion from "../../Animations/AnimatedAccordion";
import {isItemInCategoryFilter} from "../../../utils/Search";
import type {category} from "../../../screens/Amicale/Clubs/ClubListScreen";
type Props = { type Props = {
categoryRender: Function, categories: Array<category>,
categories: Array<Object>, onChipSelect: (id: number) => void,
selectedCategories: Array<number>,
} }
class ClubListHeader extends React.Component<Props> { class ClubListHeader extends React.Component<Props> {
shouldComponentUpdate(nextProps: Props) {
return nextProps.selectedCategories.length !== this.props.selectedCategories.length;
}
getChipRender = (category: category, key: string) => {
const onPress = () => this.props.onChipSelect(category.id);
return <Chip
selected={isItemInCategoryFilter(this.props.selectedCategories, [category.id])}
mode={'outlined'}
onPress={onPress}
style={{marginRight: 5, marginBottom: 5}}
key={key}
>
{category.name}
</Chip>;
};
getCategoriesRender() { getCategoriesRender() {
let final = []; let final = [];
for (let i = 0; i < this.props.categories.length; i++) { for (let i = 0; i < this.props.categories.length; i++) {
final.push(this.props.categoryRender(this.props.categories[i], this.props.categories[i].id)); final.push(this.getChipRender(this.props.categories[i], this.props.categories[i].id.toString()));
} }
return final; return final;
} }
@ -27,7 +48,7 @@ class ClubListHeader extends React.Component<Props> {
<AnimatedAccordion <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"/>}
startOpen={true} opened={true}
> >
<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}>

View file

@ -19,27 +19,16 @@ type Props = {
theme: CustomTheme, theme: CustomTheme,
} }
type State = {
expanded: boolean,
}
const LIST_ITEM_HEIGHT = 64; const LIST_ITEM_HEIGHT = 64;
class GroupListAccordion extends React.Component<Props, State> { class GroupListAccordion extends React.Component<Props> {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = {
expanded: props.item.id === "0",
}
} }
shouldComponentUpdate(nextProps: Props, nextSate: State) { shouldComponentUpdate(nextProps: Props) {
if (nextProps.currentSearchString !== this.props.currentSearchString)
this.state.expanded = nextProps.currentSearchString.length > 0;
return (nextProps.currentSearchString !== this.props.currentSearchString) return (nextProps.currentSearchString !== this.props.currentSearchString)
|| (nextSate.expanded !== this.state.expanded)
|| (nextProps.favoriteNumber !== this.props.favoriteNumber) || (nextProps.favoriteNumber !== this.props.favoriteNumber)
|| (nextProps.item.content.length !== this.props.item.content.length); || (nextProps.item.content.length !== this.props.item.content.length);
} }
@ -61,8 +50,6 @@ class GroupListAccordion extends React.Component<Props, State> {
return null; return null;
} }
itemLayout = (data, index) => ({length: LIST_ITEM_HEIGHT, offset: LIST_ITEM_HEIGHT * index, index});
render() { render() {
const item = this.props.item; const item = this.props.item;
return ( return (
@ -82,6 +69,7 @@ class GroupListAccordion extends React.Component<Props, State> {
/> />
: null} : null}
unmountWhenCollapsed={true}// Only render list if expanded for increased performance unmountWhenCollapsed={true}// Only render list if expanded for increased performance
opened={this.props.item.id === 0 || this.props.currentSearchString.length > 0}
> >
{/*$FlowFixMe*/} {/*$FlowFixMe*/}
<FlatList <FlatList
@ -91,8 +79,8 @@ class GroupListAccordion extends React.Component<Props, State> {
keyExtractor={this.keyExtractor} keyExtractor={this.keyExtractor}
listKey={item.id.toString()} listKey={item.id.toString()}
// 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> </AnimatedAccordion>
</View> </View>

View file

@ -2,7 +2,7 @@
import * as React from 'react'; import * as React from 'react';
import {Animated, Platform} from "react-native"; import {Animated, Platform} from "react-native";
import {Chip, Searchbar} from 'react-native-paper'; import {Searchbar} from 'react-native-paper';
import AuthenticatedScreen from "../../../components/Amicale/AuthenticatedScreen"; import AuthenticatedScreen from "../../../components/Amicale/AuthenticatedScreen";
import i18n from "i18n-js"; import i18n from "i18n-js";
import ClubListItem from "../../../components/Lists/Clubs/ClubListItem"; import ClubListItem from "../../../components/Lists/Clubs/ClubListItem";
@ -36,7 +36,7 @@ type Props = {
} }
type State = { type State = {
currentlySelectedCategories: Array<string>, currentlySelectedCategories: Array<number>,
currentSearchString: string, currentSearchString: string,
} }
@ -99,13 +99,11 @@ class ClubListScreen extends React.Component<Props, State> {
this.updateFilteredData(str, null); this.updateFilteredData(str, null);
}; };
keyExtractor = (item: club) => { keyExtractor = (item: club) => item.id.toString();
return item.id.toString();
};
itemLayout = (data, index) => ({length: LIST_ITEM_HEIGHT, offset: LIST_ITEM_HEIGHT * index, index}); itemLayout = (data, index) => ({length: LIST_ITEM_HEIGHT, offset: LIST_ITEM_HEIGHT * index, index});
getScreen = (data: Array<{categories: Array<category>, clubs: Array<club>} | null>) => { getScreen = (data: Array<{ categories: Array<category>, clubs: Array<club> } | null>) => {
let categoryList = []; let categoryList = [];
let clubList = []; let clubList = [];
if (data[0] != null) { if (data[0] != null) {
@ -131,11 +129,9 @@ class ClubListScreen extends React.Component<Props, State> {
) )
}; };
onChipSelect(id: string) { onChipSelect = (id: number) => this.updateFilteredData(null, id);
this.updateFilteredData(null, id);
}
updateFilteredData(filterStr: string | null, categoryId: string | null) { updateFilteredData(filterStr: string | null, categoryId: number | null) {
let newCategoriesState = [...this.state.currentlySelectedCategories]; let newCategoriesState = [...this.state.currentlySelectedCategories];
let newStrState = this.state.currentSearchString; let newStrState = this.state.currentSearchString;
if (filterStr !== null) if (filterStr !== null)
@ -154,23 +150,11 @@ class ClubListScreen extends React.Component<Props, State> {
}) })
} }
getChipRender = (category: category, key: string) => {
const onPress = this.onChipSelect.bind(this, category.id);
return <Chip
selected={isItemInCategoryFilter(this.state.currentlySelectedCategories, [category.id])}
mode={'outlined'}
onPress={onPress}
style={{marginRight: 5, marginBottom: 5}}
key={key}
>
{category.name}
</Chip>;
};
getListHeader() { getListHeader() {
return <ClubListHeader return <ClubListHeader
categories={this.categories} categories={this.categories}
categoryRender={this.getChipRender} selectedCategories={this.state.currentlySelectedCategories}
onChipSelect={this.onChipSelect}
/>; />;
} }
@ -189,7 +173,7 @@ class ClubListScreen extends React.Component<Props, State> {
return shouldRender; return shouldRender;
} }
getRenderItem = ({item}: {item: club}) => { getRenderItem = ({item}: { item: club }) => {
const onPress = this.onListItemPress.bind(this, item); const onPress = this.onListItemPress.bind(this, item);
if (this.shouldRenderItem(item)) { if (this.shouldRenderItem(item)) {
return ( return (

View file

@ -37,8 +37,8 @@ const emailRegex = /^.+@.+\..+$/;
class LoginScreen extends React.Component<Props, State> { class LoginScreen extends React.Component<Props, State> {
state = { state = {
email: '', email: 'vergnet@etud.insa-toulouse.fr',
password: '', password: 'LHSdf32ù43',
isEmailValidated: false, isEmailValidated: false,
isPasswordValidated: false, isPasswordValidated: false,
loading: false, loading: false,