forked from vergnet/application-amicale
Added support for auto hide tab bar and changed auto hide component animations
This commit is contained in:
parent
e157af57d1
commit
91853092be
10 changed files with 190 additions and 112 deletions
|
@ -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: {
|
||||
|
|
|
@ -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<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
|
||||
icon={this.props.icon}
|
||||
onPress={this.props.onPress}
|
||||
/>
|
||||
</AutoHideComponent>
|
||||
);
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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}}
|
||||
|
|
|
@ -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)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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} />}
|
||||
>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
41
src/utils/AutoHideHandler.js
Normal file
41
src/utils/AutoHideHandler.js
Normal 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;
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in a new issue