Added support for auto hide tab bar and changed auto hide component animations

This commit is contained in:
Arnaud Vergnet 2020-04-18 12:57:30 +02:00
parent e157af57d1
commit 91853092be
10 changed files with 190 additions and 112 deletions

View file

@ -3,8 +3,9 @@
import * as React from 'react'; import * as React from 'react';
import {StyleSheet, View} from "react-native"; import {StyleSheet, View} from "react-native";
import {FAB, IconButton, Surface, withTheme} from "react-native-paper"; 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 * as Animatable from 'react-native-animatable';
import CustomTabBar from "../Tabbar/CustomTabBar";
const AnimatedFAB = Animatable.createAnimatableComponent(FAB); const AnimatedFAB = Animatable.createAnimatableComponent(FAB);
@ -28,6 +29,7 @@ const DISPLAY_MODES = {
class AnimatedBottomBar extends React.Component<Props, State> { class AnimatedBottomBar extends React.Component<Props, State> {
ref: Object; ref: Object;
hideHandler: AutoHideHandler;
displayModeIcons: Object; displayModeIcons: Object;
@ -38,6 +40,9 @@ class AnimatedBottomBar extends React.Component<Props, State> {
constructor() { constructor() {
super(); super();
this.ref = React.createRef(); this.ref = React.createRef();
this.hideHandler = new AutoHideHandler(false);
this.hideHandler.addListener(this.onHideChange);
this.displayModeIcons = {}; this.displayModeIcons = {};
this.displayModeIcons[DISPLAY_MODES.DAY] = "calendar-text"; this.displayModeIcons[DISPLAY_MODES.DAY] = "calendar-text";
this.displayModeIcons[DISPLAY_MODES.WEEK] = "calendar-week"; this.displayModeIcons[DISPLAY_MODES.WEEK] = "calendar-week";
@ -49,8 +54,17 @@ class AnimatedBottomBar extends React.Component<Props, State> {
|| (nextState.currentMode !== this.state.currentMode); || (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) => { onScroll = (event: Object) => {
this.ref.current.onScroll(event); this.hideHandler.onScroll(event);
}; };
changeDisplayMode = () => { changeDisplayMode = () => {
@ -74,9 +88,13 @@ class AnimatedBottomBar extends React.Component<Props, State> {
render() { render() {
const buttonColor = this.props.theme.colors.primary; const buttonColor = this.props.theme.colors.primary;
return ( return (
<AutoHideComponent <Animatable.View
ref={this.ref} ref={this.ref}
style={styles.container}> useNativeDriver
style={{
...styles.container,
bottom: 10 + CustomTabBar.TAB_BAR_HEIGHT
}}>
<Surface style={styles.surface}> <Surface style={styles.surface}>
<View style={styles.fabContainer}> <View style={styles.fabContainer}>
<AnimatedFAB <AnimatedFAB
@ -114,7 +132,7 @@ class AnimatedBottomBar extends React.Component<Props, State> {
onPress={() => this.props.onPress('next', undefined)}/> onPress={() => this.props.onPress('next', undefined)}/>
</View> </View>
</Surface> </Surface>
</AutoHideComponent> </Animatable.View>
); );
} }
} }
@ -123,7 +141,6 @@ const styles = StyleSheet.create({
container: { container: {
position: 'absolute', position: 'absolute',
left: '5%', left: '5%',
bottom: 10,
width: '90%', width: '90%',
}, },
surface: { surface: {

View file

@ -3,42 +3,56 @@
import * as React from 'react'; import * as React from 'react';
import {StyleSheet} from "react-native"; import {StyleSheet} from "react-native";
import {FAB} from "react-native-paper"; import {FAB} from "react-native-paper";
import {AnimatedValue} from "react-native-reanimated"; import AutoHideHandler from "../../utils/AutoHideHandler";
import AutoHideComponent from "./AutoHideComponent"; import * as Animatable from 'react-native-animatable';
import CustomTabBar from "../Tabbar/CustomTabBar";
type Props = { type Props = {
navigation: Object,
icon: string, icon: string,
onPress: Function, onPress: Function,
} }
type State = { const AnimatedFab = Animatable.createAnimatableComponent(FAB);
fabPosition: AnimatedValue
}
export default class AnimatedFAB extends React.Component<Props, State> { export default class AnimatedFAB extends React.Component<Props> {
ref: Object; ref: Object;
hideHandler: AutoHideHandler;
constructor() { constructor() {
super(); super();
this.ref = React.createRef(); this.ref = React.createRef();
this.hideHandler = new AutoHideHandler(false);
this.hideHandler.addListener(this.onHideChange);
} }
onScroll = (event: Object) => { 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() { render() {
return ( return (
<AutoHideComponent <AnimatedFab
ref={this.ref} ref={this.ref}
style={styles.fab}> useNativeDriver
<FAB icon={this.props.icon}
icon={this.props.icon} onPress={this.props.onPress}
onPress={this.props.onPress} style={{
/> ...styles.fab,
</AutoHideComponent> bottom: CustomTabBar.TAB_BAR_HEIGHT
); }}
/>
);
} }
} }
@ -47,6 +61,5 @@ const styles = StyleSheet.create({
position: 'absolute', position: 'absolute',
margin: 16, margin: 16,
right: 0, right: 0,
bottom: 0,
}, },
}); });

View file

@ -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<Props, State> {
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 (
<Animated.View style={{
...this.props.style,
transform: [{translateY: this.state.fabPosition}]
}}>
{this.props.children}
</Animated.View>
);
}
}

View file

@ -9,6 +9,8 @@ import ErrorView from "../Custom/ErrorView";
import BasicLoadingScreen from "../Custom/BasicLoadingScreen"; import BasicLoadingScreen from "../Custom/BasicLoadingScreen";
import {withCollapsible} from "../../utils/withCollapsible"; import {withCollapsible} from "../../utils/withCollapsible";
import * as Animatable from 'react-native-animatable'; import * as Animatable from 'react-native-animatable';
import AutoHideHandler from "../../utils/AutoHideHandler";
import CustomTabBar from "../Tabbar/CustomTabBar";
type Props = { type Props = {
navigation: Object, navigation: Object,
@ -52,6 +54,7 @@ class WebSectionList extends React.PureComponent<Props, State> {
refreshInterval: IntervalID; refreshInterval: IntervalID;
lastRefresh: Date; lastRefresh: Date;
hideHandler: AutoHideHandler;
state = { state = {
refreshing: false, refreshing: false,
@ -72,6 +75,8 @@ class WebSectionList extends React.PureComponent<Props, State> {
this.onFetchSuccess = this.onFetchSuccess.bind(this); this.onFetchSuccess = this.onFetchSuccess.bind(this);
this.onFetchError = this.onFetchError.bind(this); this.onFetchError = this.onFetchError.bind(this);
this.getEmptySectionHeader = this.getEmptySectionHeader.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<Props, State> {
); );
} }
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() { render() {
let dataset = []; let dataset = [];
if (this.state.fetchedData !== undefined) if (this.state.fetchedData !== undefined)
@ -233,9 +248,10 @@ class WebSectionList extends React.PureComponent<Props, State> {
} }
getItemLayout={this.props.itemHeight !== null ? this.itemLayout : undefined} getItemLayout={this.props.itemHeight !== null ? this.itemLayout : undefined}
// Animations // Animations
onScroll={onScrollWithListener(this.props.onScroll)} onScroll={onScrollWithListener(this.onScroll)}
contentContainerStyle={{ contentContainerStyle={{
paddingTop: containerPaddingTop, paddingTop: containerPaddingTop,
paddingBottom: CustomTabBar.TAB_BAR_HEIGHT,
minHeight: '100%' minHeight: '100%'
}} }}
scrollIndicatorInsets={{top: scrollIndicatorInsetTop}} scrollIndicatorInsets={{top: scrollIndicatorInsetTop}}

View file

@ -11,6 +11,7 @@ import {Linking} from "expo";
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import {Animated, BackHandler} from "react-native"; import {Animated, BackHandler} from "react-native";
import {withCollapsible} from "../../utils/withCollapsible"; import {withCollapsible} from "../../utils/withCollapsible";
import AutoHideHandler from "../../utils/AutoHideHandler";
type Props = { type Props = {
navigation: Object, navigation: Object,
@ -33,6 +34,7 @@ class WebViewScreen extends React.PureComponent<Props> {
}; };
webviewRef: Object; webviewRef: Object;
hideHandler: AutoHideHandler;
canGoBack: boolean; canGoBack: boolean;
@ -40,6 +42,8 @@ class WebViewScreen extends React.PureComponent<Props> {
super(); super();
this.webviewRef = React.createRef(); this.webviewRef = React.createRef();
this.canGoBack = false; this.canGoBack = false;
this.hideHandler = new AutoHideHandler(false);
this.hideHandler.addListener(this.onHideChange);
} }
/** /**
@ -131,6 +135,16 @@ class WebViewScreen extends React.PureComponent<Props> {
); );
} }
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() { render() {
const {containerPaddingTop, onScrollWithListener} = this.props.collapsibleStack; const {containerPaddingTop, onScrollWithListener} = this.props.collapsibleStack;
return ( return (
@ -151,7 +165,7 @@ class WebViewScreen extends React.PureComponent<Props> {
onMessage={this.props.onMessage} onMessage={this.props.onMessage}
onLoad={() => this.injectJavaScript(this.getJavascriptPadding(containerPaddingTop))} onLoad={() => this.injectJavaScript(this.getJavascriptPadding(containerPaddingTop))}
// Animations // Animations
onScroll={onScrollWithListener(this.props.onScroll)} onScroll={onScrollWithListener(this.onScroll)}
/> />
); );
} }

View file

@ -11,16 +11,24 @@ type Props = {
theme: Object, theme: Object,
} }
const TAB_BAR_HEIGHT = 48;
/** /**
* Abstraction layer for Agenda component, using custom configuration * Abstraction layer for Agenda component, using custom configuration
*/ */
class CustomTabBar extends React.Component<Props> { class CustomTabBar extends React.Component<Props> {
shouldComponentUpdate(nextProps: Props): boolean { static TAB_BAR_HEIGHT = 48;
return (nextProps.theme.dark !== this.props.theme.dark)
|| (nextProps.state.index !== this.props.state.index); // 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) { onItemPress(route: Object, currentIndex: number, destIndex: number) {
@ -43,12 +51,18 @@ class CustomTabBar extends React.Component<Props> {
const navigation = this.props.navigation; const navigation = this.props.navigation;
return ( return (
<Animatable.View <Animatable.View
ref={this.tabRef}
animation={"fadeInUp"} animation={"fadeInUp"}
duration={500} duration={500}
useNativeDriver useNativeDriver
style={{ style={{
flexDirection: 'row', flexDirection: 'row',
height: TAB_BAR_HEIGHT, height: CustomTabBar.TAB_BAR_HEIGHT,
width: '100%',
position: 'absolute',
bottom: 0,
left: 0,
backgroundColor: this.props.theme.colors.surface,
}} }}
> >
{state.routes.map((route, index) => { {state.routes.map((route, index) => {
@ -70,6 +84,20 @@ class CustomTabBar extends React.Component<Props> {
target: route.key, 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 color = isFocused ? options.activeColor : options.inactiveColor;
const iconData = {focused: isFocused, color: color}; const iconData = {focused: isFocused, color: color};

View file

@ -343,9 +343,18 @@ class TabNavigator extends React.Component<Props> {
else else
return null; 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 : '', tabBarLabel: route.name !== 'home' ? undefined : '',
activeColor: this.props.theme.colors.primary, activeColor: this.props.theme.colors.primary,
inactiveColor: this.props.theme.colors.tabIcon inactiveColor: this.props.theme.colors.tabIcon,
})} })}
tabBar={props => <CustomTabBar {...props} />} tabBar={props => <CustomTabBar {...props} />}
> >

View file

@ -485,6 +485,7 @@ class HomeScreen extends React.Component<Props, State> {
onScroll={this.onScroll} onScroll={this.onScroll}
/> />
<AnimatedFAB <AnimatedFAB
{...this.props}
ref={this.fabRef} ref={this.fabRef}
icon="qrcode-scan" icon="qrcode-scan"
onPress={this.openScanner} onPress={this.openScanner}

View file

@ -9,6 +9,8 @@ import {stringMatchQuery} from "../../utils/Search";
import ProximoListItem from "../../components/Lists/ProximoListItem"; import ProximoListItem from "../../components/Lists/ProximoListItem";
import MaterialHeaderButtons, {Item} from "../../components/Custom/HeaderButton"; import MaterialHeaderButtons, {Item} from "../../components/Custom/HeaderButton";
import {withCollapsible} from "../../utils/withCollapsible"; import {withCollapsible} from "../../utils/withCollapsible";
import CustomTabBar from "../../components/Tabbar/CustomTabBar";
import AutoHideHandler from "../../utils/AutoHideHandler";
function sortPrice(a, b) { function sortPrice(a, b) {
return a.price - b.price; return a.price - b.price;
@ -57,6 +59,7 @@ class ProximoListScreen extends React.Component<Props, State> {
modalRef: Object; modalRef: Object;
listData: Array<Object>; listData: Array<Object>;
shouldFocusSearchBar: boolean; shouldFocusSearchBar: boolean;
hideHandler: AutoHideHandler;
constructor(props) { constructor(props) {
super(props); super(props);
@ -67,6 +70,8 @@ class ProximoListScreen extends React.Component<Props, State> {
currentSortMode: 3, currentSortMode: 3,
modalCurrentDisplayItem: null, modalCurrentDisplayItem: null,
}; };
this.hideHandler = new AutoHideHandler(false);
this.hideHandler.addListener(this.onHideChange);
} }
@ -296,8 +301,17 @@ class ProximoListScreen extends React.Component<Props, State> {
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});
onScroll = (event: Object) => {
this.hideHandler.onScroll(event);
};
onHideChange = (shouldHide: boolean) => {
this.props.navigation.setParams({hideTabBar: shouldHide});
}
render() { render() {
const {containerPaddingTop, scrollIndicatorInsetTop, onScroll} = this.props.collapsibleStack; const {containerPaddingTop, scrollIndicatorInsetTop, onScrollWithListener} = this.props.collapsibleStack;
return ( return (
<View style={{ <View style={{
height: '100%' height: '100%'
@ -316,8 +330,11 @@ class ProximoListScreen extends React.Component<Props, State> {
getItemLayout={this.itemLayout} getItemLayout={this.itemLayout}
initialNumToRender={10} initialNumToRender={10}
// Animations // Animations
onScroll={onScroll} onScroll={onScrollWithListener(this.onScroll)}
contentContainerStyle={{paddingTop: containerPaddingTop}} contentContainerStyle={{
paddingTop: containerPaddingTop,
paddingBottom: CustomTabBar.TAB_BAR_HEIGHT
}}
scrollIndicatorInsets={{top: scrollIndicatorInsetTop}} scrollIndicatorInsets={{top: scrollIndicatorInsetTop}}
/> />
</View> </View>

View file

@ -0,0 +1,41 @@
// @flow
import * as React from 'react';
const speedOffset = 5;
export default class AutoHideHandler {
lastOffset: number;
isHidden: boolean;
listeners: Array<Function>;
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;
}
}