From 9bd835ac90a52aa04f912e926f9810385f70349e Mon Sep 17 00:00:00 2001 From: Jon Yardley Date: Thu, 14 Jun 2018 15:16:04 +0100 Subject: [PATCH] Add some wee animation --- src/components/EventList.js | 4 + src/screens/EventsScreen/FilterHeader.js | 33 +++--- src/screens/EventsScreen/FilterHeader.test.js | 27 +++-- .../EventsScreen/ResetAllFiltersButton.js | 100 ++++++++++++++++++ src/screens/EventsScreen/component.js | 20 ++++ 5 files changed, 154 insertions(+), 30 deletions(-) create mode 100644 src/screens/EventsScreen/ResetAllFiltersButton.js diff --git a/src/components/EventList.js b/src/components/EventList.js index ed8deb08..6e5aa794 100644 --- a/src/components/EventList.js +++ b/src/components/EventList.js @@ -103,6 +103,7 @@ class EventList extends Component { sectionSeparator = () => ; keyExtractor = getId; + sectionList = null; renderItem = ({ item }: RenderItemInfo) => { const { @@ -168,6 +169,9 @@ class EventList extends Component { refreshing={refreshing} onRefresh={onRefresh} windowSize={10} + ref={sectionList => { + this.sectionList = sectionList; + }} /> ); } diff --git a/src/screens/EventsScreen/FilterHeader.js b/src/screens/EventsScreen/FilterHeader.js index 0dd6a2fe..09d88117 100644 --- a/src/screens/EventsScreen/FilterHeader.js +++ b/src/screens/EventsScreen/FilterHeader.js @@ -13,6 +13,7 @@ import { } from "../../constants/colors"; import text from "../../constants/text"; import { formatDateRange } from "../../data/formatters"; +import ResetAllFiltersButton from "./ResetAllFiltersButton"; export type Props = { onFilterCategoriesPress: Function, @@ -21,7 +22,8 @@ export type Props = { onDateFilterButtonPress: () => void, selectedCategories: Set, numTagFiltersSelected: number, - resetAllFiltersPress: () => void + resetAllFiltersPress: () => void, + scrollEventListToTop: () => void }; class FilterHeader extends React.PureComponent { @@ -37,7 +39,8 @@ class FilterHeader extends React.PureComponent { onFilterButtonPress, onDateFilterButtonPress, numTagFiltersSelected, - resetAllFiltersPress + resetAllFiltersPress, + scrollEventListToTop } = this.props; const formattedDateFilter = dateFilter ? formatDateRange(dateFilter) @@ -50,17 +53,13 @@ class FilterHeader extends React.PureComponent { - {anyAppliedFilters && ( - - - - )} + { + resetAllFiltersPress(); + scrollEventListToTop(); + }} + /> {}, onDateFilterButtonPress: () => {}, resetAllFiltersPress: () => {}, - numTagFiltersSelected: 0 + numTagFiltersSelected: 0, + scrollEventListToTop: () => {} } ) => shallow(); @@ -27,7 +28,8 @@ describe("renders correctly", () => { onFilterButtonPress: () => {}, onDateFilterButtonPress: () => {}, resetAllFiltersPress: () => {}, - numTagFiltersSelected: 0 + numTagFiltersSelected: 0, + scrollEventListToTop: () => {} }); expect(output).toMatchSnapshot(); }); @@ -40,7 +42,8 @@ describe("renders correctly", () => { onFilterButtonPress: () => {}, onDateFilterButtonPress: () => {}, resetAllFiltersPress: () => {}, - numTagFiltersSelected: 0 + numTagFiltersSelected: 0, + scrollEventListToTop: () => {} }); expect(output).toMatchSnapshot(); }); @@ -53,7 +56,8 @@ describe("renders correctly", () => { onFilterButtonPress: () => {}, onDateFilterButtonPress: () => {}, resetAllFiltersPress: () => {}, - numTagFiltersSelected: 0 + numTagFiltersSelected: 0, + scrollEventListToTop: () => {} }); expect(output).toMatchSnapshot(); }); @@ -69,7 +73,8 @@ describe("renders correctly", () => { onFilterButtonPress: () => {}, onDateFilterButtonPress: () => {}, resetAllFiltersPress: () => {}, - numTagFiltersSelected: 0 + numTagFiltersSelected: 0, + scrollEventListToTop: () => {} }); expect(output).toMatchSnapshot(); }); @@ -82,7 +87,8 @@ describe("renders correctly", () => { onFilterButtonPress: () => {}, onDateFilterButtonPress: () => {}, resetAllFiltersPress: () => {}, - numTagFiltersSelected: 2 + numTagFiltersSelected: 2, + scrollEventListToTop: () => {} }); expect(output).toMatchSnapshot(); }); @@ -98,7 +104,8 @@ describe("filter buttons", () => { onFilterButtonPress: () => {}, onDateFilterButtonPress: () => {}, resetAllFiltersPress: () => {}, - numTagFiltersSelected: 0 + numTagFiltersSelected: 0, + scrollEventListToTop: () => {} }); output.find(FilterHeaderCategories).prop("onFilterPress")(); @@ -114,7 +121,8 @@ describe("filter buttons", () => { onFilterButtonPress: () => {}, onDateFilterButtonPress: mock, resetAllFiltersPress: () => {}, - numTagFiltersSelected: 0 + numTagFiltersSelected: 0, + scrollEventListToTop: () => {} }); const button = output.find(FilterHeaderButton).at(0); button.simulate("press"); @@ -131,7 +139,8 @@ describe("filter buttons", () => { onFilterButtonPress: mock, onDateFilterButtonPress: () => {}, resetAllFiltersPress: () => {}, - numTagFiltersSelected: 0 + numTagFiltersSelected: 0, + scrollEventListToTop: () => {} }); const button = output.find(FilterHeaderButton).at(1); button.simulate("press"); diff --git a/src/screens/EventsScreen/ResetAllFiltersButton.js b/src/screens/EventsScreen/ResetAllFiltersButton.js new file mode 100644 index 00000000..9ebe26f9 --- /dev/null +++ b/src/screens/EventsScreen/ResetAllFiltersButton.js @@ -0,0 +1,100 @@ +// @flow +import React from "react"; +import { Animated, StyleSheet, Easing } from "react-native"; +import type { ViewLayoutEvent } from "react-native/Libraries/Components/View/ViewPropTypes"; +import FilterHeaderButton from "./FilterHeaderButton"; + +type Props = { + visible: boolean, + onPress: () => void +}; + +type State = { + isAnimating: boolean +}; + +const DEFAULT_HEIGHT = 120; +const DEFAULT_FADE_VALUE = 1; +const DEFAULT_TOP_OFFSET_VALUE = 1; + +class ResetAllFiltersButton extends React.Component { + constructor(props: Props) { + super(props); + this.state = { + isAnimating: false + }; + } + + setButtonHeight = (e: ViewLayoutEvent): void => { + const { height } = e.nativeEvent.layout; + this.height = height; + }; + + fadeOut(): void { + this.props.onPress(); + this.setState({ isAnimating: true }); + + Animated.parallel([ + Animated.timing(this.fadeValue, { + toValue: 0, + duration: 200 + }), + Animated.timing(this.topOffset, { + toValue: -this.height, + duration: 400, + easing: Easing.out(Easing.quad), + delay: 50 + }) + ]).start(this.animationFinished); + } + + reset(): void { + this.topOffset = new Animated.Value(DEFAULT_TOP_OFFSET_VALUE); + this.fadeValue = new Animated.Value(DEFAULT_FADE_VALUE); + } + + animationFinished = (): void => { + this.setState({ isAnimating: false }); + this.reset(); + }; + + topOffset: Animated.Value = new Animated.Value(DEFAULT_TOP_OFFSET_VALUE); + fadeValue: Animated.Value = new Animated.Value(DEFAULT_FADE_VALUE); + height: number = DEFAULT_HEIGHT; + + render() { + const isVisible: boolean = this.state.isAnimating || this.props.visible; + return ( + isVisible && ( + + this.fadeOut()} + /> + + ) + ); + } +} + +const styles = StyleSheet.create({ + clearAll: { + minHeight: 0, + paddingTop: 16 + }, + clearAllWrapper: { + flexDirection: "row", + justifyContent: "flex-end" + } +}); + +export default ResetAllFiltersButton; diff --git a/src/screens/EventsScreen/component.js b/src/screens/EventsScreen/component.js index ec4a07dd..3a6051b5 100644 --- a/src/screens/EventsScreen/component.js +++ b/src/screens/EventsScreen/component.js @@ -30,6 +30,8 @@ export type Props = { navigation: NavigationScreenProp }; +const DEFAULT_SEPARATOR_HEIGHT: number = 40; + class EventsScreen extends Component { shouldComponentUpdate(nextProps: Props) { // Intentionally do not check this.props.navigation @@ -57,6 +59,20 @@ class EventsScreen extends Component { this.props.navigation.navigate(EVENT_DATE_FILTER); }; + scrollEventListToTop = () => { + if (this.eventList && this.eventList.sectionList) { + this.eventList.sectionList.scrollToLocation({ + itemIndex: 0, + sectionIndex: 0, + viewOffset: DEFAULT_SEPARATOR_HEIGHT, + viewPosition: 0, + animated: false + }); + } + }; + + eventList = null; + render() { const { navigation, @@ -74,6 +90,7 @@ class EventsScreen extends Component { selectedCategories={this.props.selectedCategories} onFilterButtonPress={this.handleFilterButtonPress} onDateFilterButtonPress={this.handleDateFilterButtonPress} + scrollEventListToTop={this.scrollEventListToTop} /> {this.props.loading || events.length < 1 ? ( @@ -90,6 +107,9 @@ class EventsScreen extends Component { onPress={(eventId: string) => { navigation.navigate(EVENT_DETAILS, { eventId }); }} + ref={eventList => { + this.eventList = eventList; + }} /> )}