forked from vergnet/application-amicale
Improved accordion performance
This commit is contained in:
parent
1e0cc867b8
commit
070d6beb83
5 changed files with 72 additions and 65 deletions
|
@ -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 ? '0deg' : '180deg'});
|
this.chevronRef.current.transitionTo({rotate: this.state.expanded ? this.animStart : this.animEnd});
|
||||||
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}
|
||||||
|
|
|
@ -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}>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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,9 +99,7 @@ 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});
|
||||||
|
|
||||||
|
@ -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}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in a new issue