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

View file

@ -1,22 +1,43 @@
// @flow
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 i18n from 'i18n-js';
import AnimatedAccordion from "../../Animations/AnimatedAccordion";
import {isItemInCategoryFilter} from "../../../utils/Search";
import type {category} from "../../../screens/Amicale/Clubs/ClubListScreen";
type Props = {
categoryRender: Function,
categories: Array<Object>,
categories: Array<category>,
onChipSelect: (id: number) => void,
selectedCategories: Array<number>,
}
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() {
let final = [];
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;
}
@ -27,7 +48,7 @@ class ClubListHeader extends React.Component<Props> {
<AnimatedAccordion
title={i18n.t("clubs.categories")}
left={props => <List.Icon {...props} icon="star"/>}
startOpen={true}
opened={true}
>
<Text style={styles.text}>{i18n.t("clubs.categoriesFilterMessage")}</Text>
<View style={styles.chipContainer}>

View file

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

View file

@ -2,7 +2,7 @@
import * as React from 'react';
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 i18n from "i18n-js";
import ClubListItem from "../../../components/Lists/Clubs/ClubListItem";
@ -36,7 +36,7 @@ type Props = {
}
type State = {
currentlySelectedCategories: Array<string>,
currentlySelectedCategories: Array<number>,
currentSearchString: string,
}
@ -99,13 +99,11 @@ class ClubListScreen extends React.Component<Props, State> {
this.updateFilteredData(str, null);
};
keyExtractor = (item: club) => {
return item.id.toString();
};
keyExtractor = (item: club) => item.id.toString();
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 clubList = [];
if (data[0] != null) {
@ -131,11 +129,9 @@ class ClubListScreen extends React.Component<Props, State> {
)
};
onChipSelect(id: string) {
this.updateFilteredData(null, id);
}
onChipSelect = (id: number) => this.updateFilteredData(null, id);
updateFilteredData(filterStr: string | null, categoryId: string | null) {
updateFilteredData(filterStr: string | null, categoryId: number | null) {
let newCategoriesState = [...this.state.currentlySelectedCategories];
let newStrState = this.state.currentSearchString;
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() {
return <ClubListHeader
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;
}
getRenderItem = ({item}: {item: club}) => {
getRenderItem = ({item}: { item: club }) => {
const onPress = this.onListItemPress.bind(this, item);
if (this.shouldRenderItem(item)) {
return (

View file

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