Implemented collapsible header on major lists and webviews

This commit is contained in:
Arnaud Vergnet 2020-04-13 18:11:53 +02:00
parent 5d08134511
commit c67e2fa405
9 changed files with 373 additions and 163 deletions

View file

@ -51,6 +51,7 @@
"react-native-safe-area-context": "0.7.3",
"react-native-screens": "~2.2.0",
"react-native-webview": "8.1.1",
"react-navigation-collapsible": "^5.4.0",
"react-navigation-header-buttons": "^3.0.5"
},
"devDependencies": {

View file

@ -1,8 +1,8 @@
// @flow
import * as React from 'react';
import {View} from 'react-native';
import {ActivityIndicator, withTheme} from 'react-native-paper';
import {View} from "react-native";
/**
* Component used to display a header button
@ -12,17 +12,19 @@ import {View} from "react-native";
*/
function BasicLoadingScreen(props) {
const {colors} = props.theme;
let position = undefined;
if (props.isAbsolute !== undefined && props.isAbsolute)
position = 'absolute';
return (
<View style={{
backgroundColor: colors.background,
position: 'absolute',
position: position,
top: 0,
right: 0,
width: '100%',
height: '100%',
flex: 1,
alignItems: 'center',
justifyContent: 'center'
justifyContent: 'center',
}}>
<ActivityIndicator
animating={true}

View file

@ -7,6 +7,7 @@ import {Snackbar} from 'react-native-paper';
import {Animated, RefreshControl, View} from "react-native";
import ErrorView from "../Custom/ErrorView";
import BasicLoadingScreen from "../Custom/BasicLoadingScreen";
import {withCollapsible} from "../../utils/withCollapsible";
type Props = {
navigation: Object,
@ -20,6 +21,7 @@ type Props = {
updateData: number,
itemHeight: number | null,
onScroll: Function,
collapsibleStack: Object,
}
type State = {
@ -31,13 +33,14 @@ type State = {
const MIN_REFRESH_TIME = 5 * 1000;
/**
* Component used to render a SectionList with data fetched from the web
*
* This is a pure component, meaning it will only update if a shallow comparison of state and props is different.
* To force the component to update, change the value of updateData.
*/
export default class WebSectionList extends React.PureComponent<Props, State> {
class WebSectionList extends React.PureComponent<Props, State> {
static defaultProps = {
renderSectionHeader: null,
@ -171,11 +174,16 @@ export default class WebSectionList extends React.PureComponent<Props, State> {
index
});
onListScroll= (event) => {
};
render() {
let dataset = [];
if (this.state.fetchedData !== undefined)
dataset = this.props.createDataset(this.state.fetchedData);
const shouldRenderHeader = this.props.renderSectionHeader !== null;
const {containerPaddingTop, scrollIndicatorInsetTop, onScrollWithListener} = this.props.collapsibleStack;
return (
<View>
{/*$FlowFixMe*/}
@ -184,6 +192,7 @@ export default class WebSectionList extends React.PureComponent<Props, State> {
extraData={this.props.updateData}
refreshControl={
<RefreshControl
progressViewOffset={containerPaddingTop}
refreshing={this.state.refreshing}
onRefresh={this.onRefresh}
/>
@ -193,7 +202,6 @@ export default class WebSectionList extends React.PureComponent<Props, State> {
//$FlowFixMe
renderItem={this.props.renderItem}
stickySectionHeadersEnabled={this.props.stickyHeader}
contentContainerStyle={{minHeight: '100%'}}
style={{minHeight: '100%'}}
ListEmptyComponent={this.state.refreshing
? <BasicLoadingScreen/>
@ -204,7 +212,12 @@ export default class WebSectionList extends React.PureComponent<Props, State> {
}
getItemLayout={this.props.itemHeight !== null ? this.itemLayout : undefined}
// Animations
onScroll={this.props.onScroll}
onScroll={onScrollWithListener(this.props.onScroll)}
contentContainerStyle={{
paddingTop: containerPaddingTop,
minHeight: '100%'
}}
scrollIndicatorInsets={{top: scrollIndicatorInsetTop}}
/>
<Snackbar
visible={this.state.snackbarVisible}
@ -222,3 +235,5 @@ export default class WebSectionList extends React.PureComponent<Props, State> {
);
}
}
export default withCollapsible(WebSectionList);

View file

@ -9,14 +9,18 @@ import MaterialHeaderButtons, {Item} from '../Custom/HeaderButton';
import {HiddenItem} from "react-navigation-header-buttons";
import {Linking} from "expo";
import i18n from 'i18n-js';
import {BackHandler} from "react-native";
import {Animated, BackHandler} from "react-native";
import {withCollapsible} from "../../utils/withCollapsible";
type Props = {
navigation: Object,
url: string,
customJS: string,
collapsibleStack: Object,
}
const AnimatedWebView = Animated.createAnimatedComponent(WebView);
/**
* Class defining a webview screen.
*/
@ -112,15 +116,48 @@ class WebViewScreen extends React.PureComponent<Props> {
*
* @return {*}
*/
getRenderLoading = () => <BasicLoadingScreen/>;
getRenderLoading = () => <BasicLoadingScreen isAbsolute={true}/>;
// document.getElementsByTagName('body')[0].style.paddingTop = '100px';
// $( 'body *' ).filter(function(){
// var position = $(this).css('position');
// var top = $(this).css('top');
// if((position === 'fixed') && top !== 'auto'){
// console.log(top);
// $(this).css('top', 'calc(' + top + ' + 100px)');
// console.log($(this).css('top'));
// };
// });
// document.querySelectorAll('body *').forEach(function(node){
// var style = window.getComputedStyle(node);
// var position = style.getPropertyValue('position');
// var top = style.getPropertyValue('top');
// if((position === 'fixed') && top !== 'auto'){
// console.log(top);
// node.style.top = 'calc(' + top + ' + 100px)';
// console.log(node.style.top);
// console.log(node);
// };
// });
getJavascriptPadding(padding: number) {
return (
"document.getElementsByTagName('body')[0].style.paddingTop = '" + padding + "px';\n" +
"true;"
);
}
render() {
const {containerPaddingTop, onScroll} = this.props.collapsibleStack;
const customJS = this.getJavascriptPadding(containerPaddingTop);
return (
<WebView
<AnimatedWebView
ref={this.webviewRef}
source={{uri: this.props.url}}
startInLoadingState={true}
injectedJavaScript={this.props.customJS}
injectedJavaScript={this.props.customJS + customJS}
javaScriptEnabled={true}
renderLoading={this.getRenderLoading}
renderError={() => <ErrorView
@ -130,9 +167,11 @@ class WebViewScreen extends React.PureComponent<Props> {
onNavigationStateChange={navState => {
this.canGoBack = navState.canGoBack;
}}
// Animations
onScroll={onScroll}
/>
);
}
}
export default WebViewScreen;
export default withCollapsible(WebViewScreen);

View file

@ -26,6 +26,8 @@ import {AmicaleWebsiteScreen} from "../screens/Websites/AmicaleWebsiteScreen";
import {TutorInsaWebsiteScreen} from "../screens/Websites/TutorInsaWebsiteScreen";
import {WiketudWebsiteScreen} from "../screens/Websites/WiketudWebsiteScreen";
import {ElusEtudiantsWebsiteScreen} from "../screens/Websites/ElusEtudiantsWebsiteScreen";
import {createCollapsibleStack} from "react-navigation-collapsible";
import {useTheme} from "react-native-paper";
const defaultScreenOptions = {
gestureEnabled: true,
@ -106,12 +108,14 @@ function SettingsStackComponent() {
const SelfMenuStack = createStackNavigator();
function SelfMenuStackComponent() {
const {colors} = useTheme();
return (
<SelfMenuStack.Navigator
initialRouteName="self-menu"
headerMode="float"
screenOptions={defaultScreenOptions}
>
{createCollapsibleStack(
<SelfMenuStack.Screen
name="self-menu"
component={SelfMenuScreen}
@ -119,10 +123,18 @@ function SelfMenuStackComponent() {
const openDrawer = getDrawerButton.bind(this, navigation);
return {
title: i18n.t('screens.menuSelf'),
headerLeft: openDrawer
headerLeft: openDrawer,
headerStyle: {
backgroundColor: colors.surface,
},
};
}}
/>
/>,
{
collapsedColor: 'transparent',
useNativeDriver: true,
}
)}
</SelfMenuStack.Navigator>
);
}
@ -130,12 +142,14 @@ function SelfMenuStackComponent() {
const AvailableRoomStack = createStackNavigator();
function AvailableRoomStackComponent() {
const {colors} = useTheme();
return (
<AvailableRoomStack.Navigator
initialRouteName="available-rooms"
headerMode="float"
screenOptions={defaultScreenOptions}
>
{createCollapsibleStack(
<AvailableRoomStack.Screen
name="available-rooms"
component={AvailableRoomScreen}
@ -143,10 +157,18 @@ function AvailableRoomStackComponent() {
const openDrawer = getDrawerButton.bind(this, navigation);
return {
title: i18n.t('screens.availableRooms'),
headerLeft: openDrawer
headerLeft: openDrawer,
headerStyle: {
backgroundColor: colors.surface,
},
};
}}
/>
/>,
{
collapsedColor: 'transparent',
useNativeDriver: false, // native driver does not work with webview
}
)}
</AvailableRoomStack.Navigator>
);
}
@ -154,12 +176,14 @@ function AvailableRoomStackComponent() {
const BibStack = createStackNavigator();
function BibStackComponent() {
const {colors} = useTheme();
return (
<BibStack.Navigator
initialRouteName="bib"
headerMode="float"
screenOptions={defaultScreenOptions}
>
{createCollapsibleStack(
<BibStack.Screen
name="bib"
component={BibScreen}
@ -167,10 +191,18 @@ function BibStackComponent() {
const openDrawer = getDrawerButton.bind(this, navigation);
return {
title: i18n.t('screens.bib'),
headerLeft: openDrawer
headerLeft: openDrawer,
headerStyle: {
backgroundColor: colors.surface,
},
};
}}
/>
/>,
{
collapsedColor: 'transparent',
useNativeDriver: false, // native driver does not work with webview
}
)}
</BibStack.Navigator>
);
}
@ -178,12 +210,14 @@ function BibStackComponent() {
const AmicaleWebsiteStack = createStackNavigator();
function AmicaleWebsiteStackComponent() {
const {colors} = useTheme();
return (
<AmicaleWebsiteStack.Navigator
initialRouteName="amicale-website"
headerMode="float"
screenOptions={defaultScreenOptions}
>
{createCollapsibleStack(
<AmicaleWebsiteStack.Screen
name="amicale-website"
component={AmicaleWebsiteScreen}
@ -191,10 +225,18 @@ function AmicaleWebsiteStackComponent() {
const openDrawer = getDrawerButton.bind(this, navigation);
return {
title: "Amicale",
headerLeft: openDrawer
headerLeft: openDrawer,
headerStyle: {
backgroundColor: colors.surface,
},
};
}}
/>
/>,
{
collapsedColor: 'transparent',
useNativeDriver: false, // native driver does not work with webview
}
)}
</AmicaleWebsiteStack.Navigator>
);
}
@ -202,12 +244,14 @@ function AmicaleWebsiteStackComponent() {
const ElusEtudiantsStack = createStackNavigator();
function ElusEtudiantsStackComponent() {
const {colors} = useTheme();
return (
<ElusEtudiantsStack.Navigator
initialRouteName="elus-etudiants"
headerMode="float"
screenOptions={defaultScreenOptions}
>
{createCollapsibleStack(
<ElusEtudiantsStack.Screen
name="elus-etudiants"
component={ElusEtudiantsWebsiteScreen}
@ -215,10 +259,18 @@ function ElusEtudiantsStackComponent() {
const openDrawer = getDrawerButton.bind(this, navigation);
return {
title: "Élus Étudiants",
headerLeft: openDrawer
headerLeft: openDrawer,
headerStyle: {
backgroundColor: colors.surface,
},
};
}}
/>
/>,
{
collapsedColor: 'transparent',
useNativeDriver: false, // native driver does not work with webview
}
)}
</ElusEtudiantsStack.Navigator>
);
}
@ -226,12 +278,14 @@ function ElusEtudiantsStackComponent() {
const WiketudStack = createStackNavigator();
function WiketudStackComponent() {
const {colors} = useTheme();
return (
<WiketudStack.Navigator
initialRouteName="wiketud"
headerMode="float"
screenOptions={defaultScreenOptions}
>
{createCollapsibleStack(
<WiketudStack.Screen
name="wiketud"
component={WiketudWebsiteScreen}
@ -239,10 +293,18 @@ function WiketudStackComponent() {
const openDrawer = getDrawerButton.bind(this, navigation);
return {
title: "Wiketud",
headerLeft: openDrawer
headerLeft: openDrawer,
headerStyle: {
backgroundColor: colors.surface,
},
};
}}
/>
/>,
{
collapsedColor: 'transparent',
useNativeDriver: false, // native driver does not work with webview
}
)}
</WiketudStack.Navigator>
);
}
@ -250,12 +312,14 @@ function WiketudStackComponent() {
const TutorInsaStack = createStackNavigator();
function TutorInsaStackComponent() {
const {colors} = useTheme();
return (
<TutorInsaStack.Navigator
initialRouteName="tutorinsa"
headerMode="float"
screenOptions={defaultScreenOptions}
>
{createCollapsibleStack(
<TutorInsaStack.Screen
name="tutorinsa"
component={TutorInsaWebsiteScreen}
@ -263,16 +327,23 @@ function TutorInsaStackComponent() {
const openDrawer = getDrawerButton.bind(this, navigation);
return {
title: "Tutor'INSA",
headerLeft: openDrawer
headerLeft: openDrawer,
headerStyle: {
backgroundColor: colors.surface,
},
};
}}
/>
/>,
{
collapsedColor: 'transparent',
useNativeDriver: false, // native driver does not work with webview
}
)}
</TutorInsaStack.Navigator>
);
}
const TetrisStack = createStackNavigator();
function TetrisStackComponent() {
@ -408,12 +479,14 @@ function AmicaleContactStackComponent() {
const ClubStack = createStackNavigator();
function ClubStackComponent() {
const {colors} = useTheme();
return (
<ClubStack.Navigator
initialRouteName={"club-list"}
headerMode="float"
screenOptions={defaultScreenOptions}
>
{createCollapsibleStack(
<ClubStack.Screen
name="club-list"
component={ClubListScreen}
@ -421,10 +494,18 @@ function ClubStackComponent() {
const openDrawer = getDrawerButton.bind(this, navigation);
return {
title: i18n.t('clubs.clubList'),
headerLeft: openDrawer
headerLeft: openDrawer,
headerStyle: {
backgroundColor: colors.surface,
},
};
}}
/>
/>,
{
collapsedColor: 'transparent',
useNativeDriver: true,
}
)}
<ClubStack.Screen
name="club-information"
component={ClubDisplayScreen}

View file

@ -13,12 +13,13 @@ import ProximoAboutScreen from "../screens/Proximo/ProximoAboutScreen";
import PlanexScreen from '../screens/Websites/PlanexScreen';
import {MaterialCommunityIcons} from "@expo/vector-icons";
import AsyncStorageManager from "../managers/AsyncStorageManager";
import {withTheme} from 'react-native-paper';
import {useTheme, withTheme} from 'react-native-paper';
import i18n from "i18n-js";
import ClubDisplayScreen from "../screens/Amicale/Clubs/ClubDisplayScreen";
import ScannerScreen from "../screens/ScannerScreen";
import MaterialHeaderButtons, {Item} from "../components/Custom/HeaderButton";
import FeedItemScreen from "../screens/FeedItemScreen";
import {createCollapsibleStack} from "react-navigation-collapsible";
const TAB_ICONS = {
@ -46,30 +47,49 @@ function getDrawerButton(navigation: Object) {
const ProximoStack = createStackNavigator();
function ProximoStackComponent() {
const {colors} = useTheme();
return (
<ProximoStack.Navigator
initialRouteName="index"
headerMode="float"
screenOptions={defaultScreenOptions}
>
{createCollapsibleStack(
<ProximoStack.Screen
name="index"
options={({navigation}) => {
const openDrawer = getDrawerButton.bind(this, navigation);
return {
title: 'Proximo',
headerLeft: openDrawer
headerLeft: openDrawer,
headerStyle: {
backgroundColor: colors.surface,
},
};
}}
component={ProximoMainScreen}
/>
/>,
{
collapsedColor: 'transparent',
useNativeDriver: true,
}
)}
{createCollapsibleStack(
<ProximoStack.Screen
name="proximo-list"
options={{
title: i18n.t('screens.proximoArticles')
title: i18n.t('screens.proximoArticles'),
headerStyle: {
backgroundColor: colors.surface,
}
}}
component={ProximoListScreen}
/>
/>,
{
collapsedColor: 'transparent',
useNativeDriver: true,
}
)}
<ProximoStack.Screen
name="proximo-about"
component={ProximoAboutScreen}
@ -85,12 +105,14 @@ function ProximoStackComponent() {
const ProxiwashStack = createStackNavigator();
function ProxiwashStackComponent() {
const {colors} = useTheme();
return (
<ProxiwashStack.Navigator
initialRouteName="index"
headerMode='float'
screenOptions={defaultScreenOptions}
>
{createCollapsibleStack(
<ProxiwashStack.Screen
name="index"
component={ProxiwashScreen}
@ -98,10 +120,18 @@ function ProxiwashStackComponent() {
const openDrawer = getDrawerButton.bind(this, navigation);
return {
title: i18n.t('screens.proxiwash'),
headerLeft: openDrawer
headerLeft: openDrawer,
headerStyle: {
backgroundColor: colors.surface,
},
};
}}
/>
/>,
{
collapsedColor: 'transparent',
useNativeDriver: true,
}
)}
<ProxiwashStack.Screen
name="proxiwash-about"
component={ProxiwashAboutScreen}
@ -117,6 +147,7 @@ function ProxiwashStackComponent() {
const PlanningStack = createStackNavigator();
function PlanningStackComponent() {
const {colors} = useTheme();
return (
<PlanningStack.Navigator
initialRouteName="index"
@ -152,13 +183,14 @@ function HomeStackComponent(initialRoute: string | null, defaultData: Object) {
let data;
if (initialRoute !== null)
data = {data: defaultData, nextScreen: initialRoute, shouldOpen: true};
const {colors} = useTheme();
return (
<HomeStack.Navigator
initialRouteName={"index"}
headerMode="float"
screenOptions={defaultScreenOptions}
>
{createCollapsibleStack(
<HomeStack.Screen
name="index"
component={HomeScreen}
@ -166,11 +198,19 @@ function HomeStackComponent(initialRoute: string | null, defaultData: Object) {
const openDrawer = getDrawerButton.bind(this, navigation);
return {
title: i18n.t('screens.home'),
headerLeft: openDrawer
headerLeft: openDrawer,
headerStyle: {
backgroundColor: colors.surface,
},
};
}}
initialParams={data}
/>
/>,
{
collapsedColor: 'transparent',
useNativeDriver: true,
}
)}
<HomeStack.Screen
name="home-planning-information"
component={PlanningDisplayScreen}
@ -216,12 +256,14 @@ function HomeStackComponent(initialRoute: string | null, defaultData: Object) {
const PlanexStack = createStackNavigator();
function PlanexStackComponent() {
const {colors} = useTheme();
return (
<PlanexStack.Navigator
initialRouteName="index"
headerMode="float"
screenOptions={defaultScreenOptions}
>
{createCollapsibleStack(
<PlanexStack.Screen
name="index"
component={PlanexScreen}
@ -229,10 +271,18 @@ function PlanexStackComponent() {
const openDrawer = getDrawerButton.bind(this, navigation);
return {
title: 'Planex',
headerLeft: openDrawer
headerLeft: openDrawer,
headerStyle: {
backgroundColor: colors.surface,
},
};
}}
/>
/>,
{
collapsedColor: 'transparent',
useNativeDriver: false, // native driver does not work with webview
}
)}
</PlanexStack.Navigator>
);
}

View file

@ -1,7 +1,7 @@
// @flow
import * as React from 'react';
import {FlatList, Platform} from "react-native";
import {Animated, Platform} from "react-native";
import {Chip, Searchbar, withTheme} from 'react-native-paper';
import AuthenticatedScreen from "../../../components/Amicale/AuthenticatedScreen";
import i18n from "i18n-js";
@ -9,10 +9,12 @@ import ClubListItem from "../../../components/Lists/ClubListItem";
import {isItemInCategoryFilter, stringMatchQuery} from "../../../utils/Search";
import ClubListHeader from "../../../components/Lists/ClubListHeader";
import MaterialHeaderButtons, {Item} from "../../../components/Custom/HeaderButton";
import {withCollapsible} from "../../../utils/withCollapsible";
type Props = {
navigation: Object,
theme: Object,
collapsibleStack: Object,
}
type State = {
@ -94,9 +96,10 @@ class ClubListScreen extends React.Component<Props, State> {
getScreen = (data: Object) => {
this.categories = data[0].categories;
const {containerPaddingTop, scrollIndicatorInsetTop, onScroll} = this.props.collapsibleStack;
return (
//$FlowFixMe
<FlatList
<Animated.FlatList
data={data[0].clubs}
keyExtractor={this.keyExtractor}
renderItem={this.getRenderItem}
@ -104,6 +107,10 @@ class ClubListScreen extends React.Component<Props, State> {
// Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration
removeClippedSubviews={true}
getItemLayout={this.itemLayout}
// Animations
onScroll={onScroll}
contentContainerStyle={{paddingTop: containerPaddingTop}}
scrollIndicatorInsets={{top: scrollIndicatorInsetTop}}
/>
)
};
@ -208,4 +215,4 @@ class ClubListScreen extends React.Component<Props, State> {
}
}
export default withTheme(ClubListScreen);
export default withCollapsible(withTheme(ClubListScreen));

View file

@ -1,13 +1,14 @@
// @flow
import * as React from 'react';
import {FlatList, Image, Platform, ScrollView, View} from "react-native";
import {Animated, Image, Platform, ScrollView, View} from "react-native";
import i18n from "i18n-js";
import CustomModal from "../../components/Custom/CustomModal";
import {RadioButton, Searchbar, Subheading, Text, Title, withTheme} from "react-native-paper";
import {stringMatchQuery} from "../../utils/Search";
import ProximoListItem from "../../components/Lists/ProximoListItem";
import MaterialHeaderButtons, {Item} from "../../components/Custom/HeaderButton";
import {withCollapsible} from "../../utils/withCollapsible";
function sortPrice(a, b) {
return a.price - b.price;
@ -39,6 +40,7 @@ type Props = {
navigation: Object,
route: Object,
theme: Object,
collapsibleStack: Object,
}
type State = {
@ -295,6 +297,7 @@ class ProximoListScreen extends React.Component<Props, State> {
itemLayout = (data, index) => ({length: LIST_ITEM_HEIGHT, offset: LIST_ITEM_HEIGHT * index, index});
render() {
const {containerPaddingTop, scrollIndicatorInsetTop, onScroll} = this.props.collapsibleStack;
return (
<View style={{
height: '100%'
@ -303,7 +306,7 @@ class ProximoListScreen extends React.Component<Props, State> {
{this.state.modalCurrentDisplayItem}
</CustomModal>
{/*$FlowFixMe*/}
<FlatList
<Animated.FlatList
data={this.listData}
extraData={this.state.currentSearchString + this.state.currentSortMode}
keyExtractor={this.keyExtractor}
@ -312,10 +315,14 @@ class ProximoListScreen extends React.Component<Props, State> {
removeClippedSubviews={true}
getItemLayout={this.itemLayout}
initialNumToRender={10}
// Animations
onScroll={onScroll}
contentContainerStyle={{paddingTop: containerPaddingTop}}
scrollIndicatorInsets={{top: scrollIndicatorInsetTop}}
/>
</View>
);
}
}
export default withTheme(ProximoListScreen);
export default withCollapsible(withTheme(ProximoListScreen));

View file

@ -0,0 +1,8 @@
import React from 'react';
import {useCollapsibleStack} from "react-navigation-collapsible";
export const withCollapsible = (Component: any) => {
return (props: any) => {
return <Component collapsibleStack={useCollapsibleStack()} {...props} />;
};
};