From 91853092becf8a20edf5ba232d9130911bd1a421 Mon Sep 17 00:00:00 2001 From: Arnaud Vergnet Date: Sat, 18 Apr 2020 12:57:30 +0200 Subject: [PATCH] Added support for auto hide tab bar and changed auto hide component animations --- src/components/Custom/AnimatedBottomBar.js | 29 ++++++-- src/components/Custom/AnimatedFAB.js | 45 ++++++++----- src/components/Custom/AutoHideComponent.js | 78 ---------------------- src/components/Lists/WebSectionList.js | 18 ++++- src/components/Screens/WebViewScreen.js | 16 ++++- src/components/Tabbar/CustomTabBar.js | 40 +++++++++-- src/navigation/MainTabNavigator.js | 11 ++- src/screens/HomeScreen.js | 1 + src/screens/Proximo/ProximoListScreen.js | 23 ++++++- src/utils/AutoHideHandler.js | 41 ++++++++++++ 10 files changed, 190 insertions(+), 112 deletions(-) delete mode 100644 src/components/Custom/AutoHideComponent.js create mode 100644 src/utils/AutoHideHandler.js diff --git a/src/components/Custom/AnimatedBottomBar.js b/src/components/Custom/AnimatedBottomBar.js index 653d91c..b14f561 100644 --- a/src/components/Custom/AnimatedBottomBar.js +++ b/src/components/Custom/AnimatedBottomBar.js @@ -3,8 +3,9 @@ import * as React from 'react'; import {StyleSheet, View} from "react-native"; import {FAB, IconButton, Surface, withTheme} from "react-native-paper"; -import AutoHideComponent from "./AutoHideComponent"; +import AutoHideHandler from "../../utils/AutoHideHandler"; import * as Animatable from 'react-native-animatable'; +import CustomTabBar from "../Tabbar/CustomTabBar"; const AnimatedFAB = Animatable.createAnimatableComponent(FAB); @@ -28,6 +29,7 @@ const DISPLAY_MODES = { class AnimatedBottomBar extends React.Component { ref: Object; + hideHandler: AutoHideHandler; displayModeIcons: Object; @@ -38,6 +40,9 @@ class AnimatedBottomBar extends React.Component { constructor() { super(); this.ref = React.createRef(); + this.hideHandler = new AutoHideHandler(false); + this.hideHandler.addListener(this.onHideChange); + this.displayModeIcons = {}; this.displayModeIcons[DISPLAY_MODES.DAY] = "calendar-text"; this.displayModeIcons[DISPLAY_MODES.WEEK] = "calendar-week"; @@ -49,8 +54,17 @@ class AnimatedBottomBar extends React.Component { || (nextState.currentMode !== this.state.currentMode); } + onHideChange = (shouldHide: boolean) => { + if (this.ref.current) { + if (shouldHide) + this.ref.current.fadeOutDown(600); + else + this.ref.current.fadeInUp(500); + } + } + onScroll = (event: Object) => { - this.ref.current.onScroll(event); + this.hideHandler.onScroll(event); }; changeDisplayMode = () => { @@ -74,9 +88,13 @@ class AnimatedBottomBar extends React.Component { render() { const buttonColor = this.props.theme.colors.primary; return ( - + useNativeDriver + style={{ + ...styles.container, + bottom: 10 + CustomTabBar.TAB_BAR_HEIGHT + }}> { onPress={() => this.props.onPress('next', undefined)}/> - + ); } } @@ -123,7 +141,6 @@ const styles = StyleSheet.create({ container: { position: 'absolute', left: '5%', - bottom: 10, width: '90%', }, surface: { diff --git a/src/components/Custom/AnimatedFAB.js b/src/components/Custom/AnimatedFAB.js index db918db..2cfba3d 100644 --- a/src/components/Custom/AnimatedFAB.js +++ b/src/components/Custom/AnimatedFAB.js @@ -3,42 +3,56 @@ import * as React from 'react'; import {StyleSheet} from "react-native"; import {FAB} from "react-native-paper"; -import {AnimatedValue} from "react-native-reanimated"; -import AutoHideComponent from "./AutoHideComponent"; +import AutoHideHandler from "../../utils/AutoHideHandler"; +import * as Animatable from 'react-native-animatable'; +import CustomTabBar from "../Tabbar/CustomTabBar"; type Props = { + navigation: Object, icon: string, onPress: Function, } -type State = { - fabPosition: AnimatedValue -} +const AnimatedFab = Animatable.createAnimatableComponent(FAB); -export default class AnimatedFAB extends React.Component { +export default class AnimatedFAB extends React.Component { ref: Object; + hideHandler: AutoHideHandler; constructor() { super(); this.ref = React.createRef(); + this.hideHandler = new AutoHideHandler(false); + this.hideHandler.addListener(this.onHideChange); } onScroll = (event: Object) => { - this.ref.current.onScroll(event); + this.hideHandler.onScroll(event); }; + onHideChange = (shouldHide: boolean) => { + if (this.ref.current) { + if (shouldHide) + this.ref.current.bounceOutDown(1000); + else + this.ref.current.bounceInUp(1000); + } + } + render() { return ( - - - - ); + useNativeDriver + icon={this.props.icon} + onPress={this.props.onPress} + style={{ + ...styles.fab, + bottom: CustomTabBar.TAB_BAR_HEIGHT + }} + /> + ); } } @@ -47,6 +61,5 @@ const styles = StyleSheet.create({ position: 'absolute', margin: 16, right: 0, - bottom: 0, }, }); diff --git a/src/components/Custom/AutoHideComponent.js b/src/components/Custom/AutoHideComponent.js deleted file mode 100644 index 2768414..0000000 --- a/src/components/Custom/AutoHideComponent.js +++ /dev/null @@ -1,78 +0,0 @@ -// @flow - -import * as React from 'react'; -import {Animated} from 'react-native' -import {AnimatedValue} from "react-native-reanimated"; - -type Props = { - children: React.Node, - style: Object, -} - -type State = { - fabPosition: AnimatedValue -} - -export default class AutoHideComponent extends React.Component { - - isAnimationDownPlaying: boolean; - isAnimationUpPlaying: boolean; - - downAnimation; - upAnimation; - - lastOffset: number; - - state = { - fabPosition: new Animated.Value(0), - }; - - constructor() { - super(); - } - - onScroll({nativeEvent}: Object) { - const speed = nativeEvent.contentOffset.y < 0 ? 0 : this.lastOffset - nativeEvent.contentOffset.y; - if (speed < -5) { // Go down - if (!this.isAnimationDownPlaying) { - this.isAnimationDownPlaying = true; - if (this.isAnimationUpPlaying) - this.upAnimation.stop(); - this.downAnimation = Animated.spring(this.state.fabPosition, { - toValue: 100, - duration: 50, - useNativeDriver: true, - }); - this.downAnimation.start(() => { - this.isAnimationDownPlaying = false - }); - } - } else if (speed > 5) { // Go up - if (!this.isAnimationUpPlaying) { - this.isAnimationUpPlaying = true; - if (this.isAnimationDownPlaying) - this.downAnimation.stop(); - this.upAnimation = Animated.spring(this.state.fabPosition, { - toValue: 0, - duration: 50, - useNativeDriver: true, - }); - this.upAnimation.start(() => { - this.isAnimationUpPlaying = false - }); - } - } - this.lastOffset = nativeEvent.contentOffset.y; - } - - render() { - return ( - - {this.props.children} - - ); - } -} \ No newline at end of file diff --git a/src/components/Lists/WebSectionList.js b/src/components/Lists/WebSectionList.js index a3990bf..bea52a4 100644 --- a/src/components/Lists/WebSectionList.js +++ b/src/components/Lists/WebSectionList.js @@ -9,6 +9,8 @@ import ErrorView from "../Custom/ErrorView"; import BasicLoadingScreen from "../Custom/BasicLoadingScreen"; import {withCollapsible} from "../../utils/withCollapsible"; import * as Animatable from 'react-native-animatable'; +import AutoHideHandler from "../../utils/AutoHideHandler"; +import CustomTabBar from "../Tabbar/CustomTabBar"; type Props = { navigation: Object, @@ -52,6 +54,7 @@ class WebSectionList extends React.PureComponent { refreshInterval: IntervalID; lastRefresh: Date; + hideHandler: AutoHideHandler; state = { refreshing: false, @@ -72,6 +75,8 @@ class WebSectionList extends React.PureComponent { this.onFetchSuccess = this.onFetchSuccess.bind(this); this.onFetchError = this.onFetchError.bind(this); this.getEmptySectionHeader = this.getEmptySectionHeader.bind(this); + this.hideHandler = new AutoHideHandler(false); + this.hideHandler.addListener(this.onHideChange); } /** @@ -199,6 +204,16 @@ class WebSectionList extends React.PureComponent { ); } + onScroll = (event: Object) => { + this.hideHandler.onScroll(event); + if (this.props.onScroll) + this.props.onScroll(event); + } + + onHideChange = (shouldHide: boolean) => { + this.props.navigation.setParams({hideTabBar: shouldHide}); + } + render() { let dataset = []; if (this.state.fetchedData !== undefined) @@ -233,9 +248,10 @@ class WebSectionList extends React.PureComponent { } getItemLayout={this.props.itemHeight !== null ? this.itemLayout : undefined} // Animations - onScroll={onScrollWithListener(this.props.onScroll)} + onScroll={onScrollWithListener(this.onScroll)} contentContainerStyle={{ paddingTop: containerPaddingTop, + paddingBottom: CustomTabBar.TAB_BAR_HEIGHT, minHeight: '100%' }} scrollIndicatorInsets={{top: scrollIndicatorInsetTop}} diff --git a/src/components/Screens/WebViewScreen.js b/src/components/Screens/WebViewScreen.js index 043989a..56b988c 100644 --- a/src/components/Screens/WebViewScreen.js +++ b/src/components/Screens/WebViewScreen.js @@ -11,6 +11,7 @@ import {Linking} from "expo"; import i18n from 'i18n-js'; import {Animated, BackHandler} from "react-native"; import {withCollapsible} from "../../utils/withCollapsible"; +import AutoHideHandler from "../../utils/AutoHideHandler"; type Props = { navigation: Object, @@ -33,6 +34,7 @@ class WebViewScreen extends React.PureComponent { }; webviewRef: Object; + hideHandler: AutoHideHandler; canGoBack: boolean; @@ -40,6 +42,8 @@ class WebViewScreen extends React.PureComponent { super(); this.webviewRef = React.createRef(); this.canGoBack = false; + this.hideHandler = new AutoHideHandler(false); + this.hideHandler.addListener(this.onHideChange); } /** @@ -131,6 +135,16 @@ class WebViewScreen extends React.PureComponent { ); } + onScroll = (event: Object) => { + this.hideHandler.onScroll(event); + if (this.props.onScroll) + this.props.onScroll(event); + } + + onHideChange = (shouldHide: boolean) => { + this.props.navigation.setParams({hideTabBar: shouldHide}); + } + render() { const {containerPaddingTop, onScrollWithListener} = this.props.collapsibleStack; return ( @@ -151,7 +165,7 @@ class WebViewScreen extends React.PureComponent { onMessage={this.props.onMessage} onLoad={() => this.injectJavaScript(this.getJavascriptPadding(containerPaddingTop))} // Animations - onScroll={onScrollWithListener(this.props.onScroll)} + onScroll={onScrollWithListener(this.onScroll)} /> ); } diff --git a/src/components/Tabbar/CustomTabBar.js b/src/components/Tabbar/CustomTabBar.js index ba4b87a..2b66647 100644 --- a/src/components/Tabbar/CustomTabBar.js +++ b/src/components/Tabbar/CustomTabBar.js @@ -11,16 +11,24 @@ type Props = { theme: Object, } -const TAB_BAR_HEIGHT = 48; - /** * Abstraction layer for Agenda component, using custom configuration */ class CustomTabBar extends React.Component { - shouldComponentUpdate(nextProps: Props): boolean { - return (nextProps.theme.dark !== this.props.theme.dark) - || (nextProps.state.index !== this.props.state.index); + static TAB_BAR_HEIGHT = 48; + + // shouldComponentUpdate(nextProps: Props): boolean { + // return (nextProps.theme.dark !== this.props.theme.dark) + // || (nextProps.state.index !== this.props.state.index); + // } + + isHidden: boolean; + tabRef: Object; + + constructor() { + super(); + this.tabRef = React.createRef(); } onItemPress(route: Object, currentIndex: number, destIndex: number) { @@ -43,12 +51,18 @@ class CustomTabBar extends React.Component { const navigation = this.props.navigation; return ( {state.routes.map((route, index) => { @@ -70,6 +84,20 @@ class CustomTabBar extends React.Component { target: route.key, }); }; + if (isFocused) { + const tabVisible = options.tabBarVisible(); + console.log(tabVisible); + if (this.tabRef.current) { + if (this.isHidden && tabVisible) { + this.isHidden = false; + this.tabRef.current.slideInUp(300); + } else if (!this.isHidden && !tabVisible){ + this.isHidden = true; + this.tabRef.current.slideOutDown(300); + } + } + + } const color = isFocused ? options.activeColor : options.inactiveColor; const iconData = {focused: isFocused, color: color}; diff --git a/src/navigation/MainTabNavigator.js b/src/navigation/MainTabNavigator.js index a7a4b10..6b0ce77 100644 --- a/src/navigation/MainTabNavigator.js +++ b/src/navigation/MainTabNavigator.js @@ -343,9 +343,18 @@ class TabNavigator extends React.Component { else return null; }, + tabBarVisible: () => { + const state = route.state; + // Get the current route in the stack + const screen = state ? state.routes[state.index] : undefined; + const params = screen ? screen.params : undefined; + const hideTabBar = params ? params.hideTabBar : undefined; + return hideTabBar !== undefined ? !hideTabBar : true; + }, + animationEnabled: true, tabBarLabel: route.name !== 'home' ? undefined : '', activeColor: this.props.theme.colors.primary, - inactiveColor: this.props.theme.colors.tabIcon + inactiveColor: this.props.theme.colors.tabIcon, })} tabBar={props => } > diff --git a/src/screens/HomeScreen.js b/src/screens/HomeScreen.js index 3c89d67..cfce94e 100644 --- a/src/screens/HomeScreen.js +++ b/src/screens/HomeScreen.js @@ -485,6 +485,7 @@ class HomeScreen extends React.Component { onScroll={this.onScroll} /> { modalRef: Object; listData: Array; shouldFocusSearchBar: boolean; + hideHandler: AutoHideHandler; constructor(props) { super(props); @@ -67,6 +70,8 @@ class ProximoListScreen extends React.Component { currentSortMode: 3, modalCurrentDisplayItem: null, }; + this.hideHandler = new AutoHideHandler(false); + this.hideHandler.addListener(this.onHideChange); } @@ -296,8 +301,17 @@ class ProximoListScreen extends React.Component { itemLayout = (data, index) => ({length: LIST_ITEM_HEIGHT, offset: LIST_ITEM_HEIGHT * index, index}); + + onScroll = (event: Object) => { + this.hideHandler.onScroll(event); + }; + + onHideChange = (shouldHide: boolean) => { + this.props.navigation.setParams({hideTabBar: shouldHide}); + } + render() { - const {containerPaddingTop, scrollIndicatorInsetTop, onScroll} = this.props.collapsibleStack; + const {containerPaddingTop, scrollIndicatorInsetTop, onScrollWithListener} = this.props.collapsibleStack; return ( { getItemLayout={this.itemLayout} initialNumToRender={10} // Animations - onScroll={onScroll} - contentContainerStyle={{paddingTop: containerPaddingTop}} + onScroll={onScrollWithListener(this.onScroll)} + contentContainerStyle={{ + paddingTop: containerPaddingTop, + paddingBottom: CustomTabBar.TAB_BAR_HEIGHT + }} scrollIndicatorInsets={{top: scrollIndicatorInsetTop}} /> diff --git a/src/utils/AutoHideHandler.js b/src/utils/AutoHideHandler.js new file mode 100644 index 0000000..dbb7740 --- /dev/null +++ b/src/utils/AutoHideHandler.js @@ -0,0 +1,41 @@ +// @flow + +import * as React from 'react'; + +const speedOffset = 5; + +export default class AutoHideHandler { + + lastOffset: number; + isHidden: boolean; + + listeners: Array; + + constructor(startHidden: boolean) { + this.listeners = []; + this.isHidden = startHidden; + } + + addListener(listener: Function) { + this.listeners.push(listener); + } + + notifyListeners(shouldHide: boolean) { + for (let i = 0; i < this.listeners.length; i++) { + this.listeners[i](shouldHide); + } + } + + onScroll({nativeEvent}: Object) { + const speed = nativeEvent.contentOffset.y < 0 ? 0 : this.lastOffset - nativeEvent.contentOffset.y; + if (speed < -speedOffset && !this.isHidden) { // Go down + this.notifyListeners(true); + this.isHidden = true; + } else if (speed > speedOffset && this.isHidden) { // Go up + this.notifyListeners(false); + this.isHidden = false; + } + this.lastOffset = nativeEvent.contentOffset.y; + } + +} \ No newline at end of file