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 {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<Props, State> {
ref: Object;
hideHandler: AutoHideHandler;
displayModeIcons: Object;
@ -38,6 +40,9 @@ class AnimatedBottomBar extends React.Component<Props, State> {
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<Props, State> {
|| (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<Props, State> {
render() {
const buttonColor = this.props.theme.colors.primary;
return (
<AutoHideComponent
<Animatable.View
ref={this.ref}
style={styles.container}>
useNativeDriver
style={{
...styles.container,
bottom: 10 + CustomTabBar.TAB_BAR_HEIGHT
}}>
<Surface style={styles.surface}>
<View style={styles.fabContainer}>
<AnimatedFAB
@ -114,7 +132,7 @@ class AnimatedBottomBar extends React.Component<Props, State> {
onPress={() => this.props.onPress('next', undefined)}/>
</View>
</Surface>
</AutoHideComponent>
</Animatable.View>
);
}
}
@ -123,7 +141,6 @@ const styles = StyleSheet.create({
container: {
position: 'absolute',
left: '5%',
bottom: 10,
width: '90%',
},
surface: {

View file

@ -3,41 +3,55 @@
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<Props, State> {
export default class AnimatedFAB extends React.Component<Props> {
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 (
<AutoHideComponent
<AnimatedFab
ref={this.ref}
style={styles.fab}>
<FAB
useNativeDriver
icon={this.props.icon}
onPress={this.props.onPress}
style={{
...styles.fab,
bottom: CustomTabBar.TAB_BAR_HEIGHT
}}
/>
</AutoHideComponent>
);
}
}
@ -47,6 +61,5 @@ const styles = StyleSheet.create({
position: 'absolute',
margin: 16,
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 {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<Props, State> {
refreshInterval: IntervalID;
lastRefresh: Date;
hideHandler: AutoHideHandler;
state = {
refreshing: false,
@ -72,6 +75,8 @@ class WebSectionList extends React.PureComponent<Props, State> {
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<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() {
let dataset = [];
if (this.state.fetchedData !== undefined)
@ -233,9 +248,10 @@ class WebSectionList extends React.PureComponent<Props, State> {
}
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}}

View file

@ -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<Props> {
};
webviewRef: Object;
hideHandler: AutoHideHandler;
canGoBack: boolean;
@ -40,6 +42,8 @@ class WebViewScreen extends React.PureComponent<Props> {
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<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() {
const {containerPaddingTop, onScrollWithListener} = this.props.collapsibleStack;
return (
@ -151,7 +165,7 @@ class WebViewScreen extends React.PureComponent<Props> {
onMessage={this.props.onMessage}
onLoad={() => this.injectJavaScript(this.getJavascriptPadding(containerPaddingTop))}
// Animations
onScroll={onScrollWithListener(this.props.onScroll)}
onScroll={onScrollWithListener(this.onScroll)}
/>
);
}

View file

@ -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<Props> {
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<Props> {
const navigation = this.props.navigation;
return (
<Animatable.View
ref={this.tabRef}
animation={"fadeInUp"}
duration={500}
useNativeDriver
style={{
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) => {
@ -70,6 +84,20 @@ class CustomTabBar extends React.Component<Props> {
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};

View file

@ -343,9 +343,18 @@ class TabNavigator extends React.Component<Props> {
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 => <CustomTabBar {...props} />}
>

View file

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

View file

@ -9,6 +9,8 @@ import {stringMatchQuery} from "../../utils/Search";
import ProximoListItem from "../../components/Lists/ProximoListItem";
import MaterialHeaderButtons, {Item} from "../../components/Custom/HeaderButton";
import {withCollapsible} from "../../utils/withCollapsible";
import CustomTabBar from "../../components/Tabbar/CustomTabBar";
import AutoHideHandler from "../../utils/AutoHideHandler";
function sortPrice(a, b) {
return a.price - b.price;
@ -57,6 +59,7 @@ class ProximoListScreen extends React.Component<Props, State> {
modalRef: Object;
listData: Array<Object>;
shouldFocusSearchBar: boolean;
hideHandler: AutoHideHandler;
constructor(props) {
super(props);
@ -67,6 +70,8 @@ class ProximoListScreen extends React.Component<Props, State> {
currentSortMode: 3,
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});
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 (
<View style={{
height: '100%'
@ -316,8 +330,11 @@ class ProximoListScreen extends React.Component<Props, State> {
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}}
/>
</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;
}
}