Add scroll-to-top on back
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
63
src/components/hideableHeader.tsx
Normal file
63
src/components/hideableHeader.tsx
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import React, { useEffect, useRef } from 'react';
|
||||||
|
import { Animated, StyleSheet } from 'react-native';
|
||||||
|
import { useDimensions } from '../contexts';
|
||||||
|
import styles from '../styles';
|
||||||
|
import { useTheme } from 'react-native-paper';
|
||||||
|
|
||||||
|
const hideableHeaderStyles = StyleSheet.create({
|
||||||
|
headerView: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
zIndex: 100,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const HideableHeader = ({
|
||||||
|
visible = true,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
visible?: boolean;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) => {
|
||||||
|
const { colors } = useTheme();
|
||||||
|
const { orientation } = useDimensions();
|
||||||
|
|
||||||
|
const headerAnim = useRef(new Animated.Value(visible ? 1 : 0)).current;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
Animated.timing(headerAnim, {
|
||||||
|
toValue: visible ? 1 : 0,
|
||||||
|
duration: visible ? 200 : 150,
|
||||||
|
useNativeDriver: true,
|
||||||
|
}).start();
|
||||||
|
}, [headerAnim, visible]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Animated.View
|
||||||
|
style={[
|
||||||
|
hideableHeaderStyles.headerView,
|
||||||
|
orientation == 'portrait' && styles.paddingTop,
|
||||||
|
orientation == 'landscape' && styles.smallPaddingTop,
|
||||||
|
styles.paddingHorizontal,
|
||||||
|
{
|
||||||
|
transform: [
|
||||||
|
{
|
||||||
|
translateY: headerAnim.interpolate({
|
||||||
|
inputRange: [0, 1],
|
||||||
|
outputRange: [-130, 0],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
backgroundColor: colors.background,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
{children}
|
||||||
|
</Animated.View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HideableHeader;
|
@@ -1,5 +1,6 @@
|
|||||||
export { default as FloatingActionButton } from './floatingActionButton';
|
export { default as FloatingActionButton } from './floatingActionButton';
|
||||||
export { default as HideableBottomNavigationBar } from './hideableBottomNavigationBar';
|
export { default as HideableBottomNavigationBar } from './hideableBottomNavigationBar';
|
||||||
|
export { default as HideableHeader } from './hideableHeader';
|
||||||
export { default as LoadingView } from './loadingView';
|
export { default as LoadingView } from './loadingView';
|
||||||
export { default as TagChip } from './tagChip';
|
export { default as TagChip } from './tags/tagChip';
|
||||||
export { default as TagPreview } from './tagPreview';
|
export { default as TagPreview } from './tags/tagPreview';
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { getContrastColor } from '../utilities';
|
import { getContrastColor } from '../../utilities';
|
||||||
import { Chip } from 'react-native-paper';
|
import { Chip } from 'react-native-paper';
|
||||||
import { Tag } from '../database';
|
import { Tag } from '../../database';
|
||||||
import FontAwesome5 from 'react-native-vector-icons/FontAwesome5';
|
import FontAwesome5 from 'react-native-vector-icons/FontAwesome5';
|
||||||
|
|
||||||
const TagChip = ({ tag }: { tag: Tag }) => {
|
const TagChip = ({ tag }: { tag: Tag }) => {
|
@@ -2,9 +2,9 @@ import React from 'react';
|
|||||||
import { StyleSheet, View } from 'react-native';
|
import { StyleSheet, View } from 'react-native';
|
||||||
import FontAwesome5 from 'react-native-vector-icons/FontAwesome5';
|
import FontAwesome5 from 'react-native-vector-icons/FontAwesome5';
|
||||||
import { Chip } from 'react-native-paper';
|
import { Chip } from 'react-native-paper';
|
||||||
import styles from '../styles';
|
import styles from '../../styles';
|
||||||
import { useDimensions } from '../contexts';
|
import { useDimensions } from '../../contexts';
|
||||||
import { getContrastColor } from '../utilities';
|
import { getContrastColor } from '../../utilities';
|
||||||
|
|
||||||
const tagPreviewStyles = StyleSheet.create({
|
const tagPreviewStyles = StyleSheet.create({
|
||||||
chip: {
|
chip: {
|
@@ -1,11 +1,11 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
NativeSyntheticEvent,
|
NativeSyntheticEvent,
|
||||||
NativeScrollEvent,
|
NativeScrollEvent,
|
||||||
Animated,
|
BackHandler,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@@ -19,7 +19,12 @@ import {
|
|||||||
import { useQuery } from '@realm/react';
|
import { useQuery } from '@realm/react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { FlashList } from '@shopify/flash-list';
|
import { FlashList } from '@shopify/flash-list';
|
||||||
import { TagChip } from '../components';
|
import {
|
||||||
|
NavigationProp,
|
||||||
|
useFocusEffect,
|
||||||
|
useNavigation,
|
||||||
|
} from '@react-navigation/native';
|
||||||
|
import { HideableHeader, TagChip } from '../components';
|
||||||
import { Tag } from '../database';
|
import { Tag } from '../database';
|
||||||
import styles from '../styles';
|
import styles from '../styles';
|
||||||
import {
|
import {
|
||||||
@@ -37,17 +42,8 @@ import {
|
|||||||
tagSortQuery,
|
tagSortQuery,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { getSortIcon } from '../utilities';
|
import { getSortIcon } from '../utilities';
|
||||||
import { useDimensions } from '../contexts';
|
|
||||||
import { NavigationProp, useNavigation } from '@react-navigation/native';
|
|
||||||
|
|
||||||
const tagsStyles = StyleSheet.create({
|
const tagsStyles = StyleSheet.create({
|
||||||
headerView: {
|
|
||||||
position: 'absolute',
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
zIndex: 100,
|
|
||||||
},
|
|
||||||
headerButtonView: {
|
headerButtonView: {
|
||||||
height: 50,
|
height: 50,
|
||||||
},
|
},
|
||||||
@@ -74,8 +70,7 @@ const tagsStyles = StyleSheet.create({
|
|||||||
|
|
||||||
const Tags = () => {
|
const Tags = () => {
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
const { orientation } = useDimensions();
|
const { navigate } = useNavigation<NavigationProp<RootStackParamList>>();
|
||||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
|
||||||
const sort = useSelector((state: RootState) => state.tags.sort);
|
const sort = useSelector((state: RootState) => state.tags.sort);
|
||||||
const sortDirection = useSelector(
|
const sortDirection = useSelector(
|
||||||
(state: RootState) => state.tags.sortDirection,
|
(state: RootState) => state.tags.sortDirection,
|
||||||
@@ -101,16 +96,6 @@ const Tags = () => {
|
|||||||
setSortMenuVisible(false);
|
setSortMenuVisible(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const sortMenuAnim = useRef(new Animated.Value(navVisisble ? 1 : 0)).current;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
Animated.timing(sortMenuAnim, {
|
|
||||||
toValue: navVisisble ? 1 : 0,
|
|
||||||
duration: navVisisble ? 200 : 150,
|
|
||||||
useNativeDriver: true,
|
|
||||||
}).start();
|
|
||||||
}, [navVisisble, sortMenuAnim]);
|
|
||||||
|
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
|
|
||||||
const tags = useQuery<Tag>(
|
const tags = useQuery<Tag>(
|
||||||
@@ -132,17 +117,33 @@ const Tags = () => {
|
|||||||
|
|
||||||
if (currentOffset <= 150) {
|
if (currentOffset <= 150) {
|
||||||
dispatch(setNavVisible(true));
|
dispatch(setNavVisible(true));
|
||||||
setScrollOffset(0);
|
} else {
|
||||||
return;
|
const diff = currentOffset - scrollOffset;
|
||||||
|
if (Math.abs(diff) > 50) dispatch(setNavVisible(diff < 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
const diff = currentOffset - scrollOffset;
|
|
||||||
if (Math.abs(diff) < 50) return;
|
|
||||||
|
|
||||||
dispatch(setNavVisible(diff < 0));
|
|
||||||
setScrollOffset(currentOffset);
|
setScrollOffset(currentOffset);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const flashListRef = useRef<FlashList<Tag>>(null);
|
||||||
|
|
||||||
|
useFocusEffect(
|
||||||
|
useCallback(() => {
|
||||||
|
const handleBackPress = () => {
|
||||||
|
if (scrollOffset > 0) {
|
||||||
|
flashListRef.current?.scrollToOffset({ offset: 0, animated: true });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
BackHandler.addEventListener('hardwareBackPress', handleBackPress);
|
||||||
|
|
||||||
|
return () =>
|
||||||
|
BackHandler.removeEventListener('hardwareBackPress', handleBackPress);
|
||||||
|
}, [flashListRef, scrollOffset]),
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
@@ -150,26 +151,7 @@ const Tags = () => {
|
|||||||
styles.fullSize,
|
styles.fullSize,
|
||||||
{ backgroundColor: colors.background },
|
{ backgroundColor: colors.background },
|
||||||
]}>
|
]}>
|
||||||
<Animated.View
|
<HideableHeader visible={navVisisble}>
|
||||||
style={[
|
|
||||||
tagsStyles.headerView,
|
|
||||||
orientation == 'portrait' && styles.paddingTop,
|
|
||||||
orientation == 'landscape' && styles.smallPaddingTop,
|
|
||||||
styles.paddingHorizontal,
|
|
||||||
{
|
|
||||||
transform: [
|
|
||||||
{
|
|
||||||
translateY: sortMenuAnim.interpolate({
|
|
||||||
inputRange: [0, 1],
|
|
||||||
outputRange: [-130, 0],
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
backgroundColor: colors.background,
|
|
||||||
},
|
|
||||||
]}>
|
|
||||||
<Searchbar
|
<Searchbar
|
||||||
placeholder="Search Tags"
|
placeholder="Search Tags"
|
||||||
value={search}
|
value={search}
|
||||||
@@ -209,15 +191,16 @@ const Tags = () => {
|
|||||||
</Menu>
|
</Menu>
|
||||||
</View>
|
</View>
|
||||||
<Divider />
|
<Divider />
|
||||||
</Animated.View>
|
</HideableHeader>
|
||||||
<FlashList
|
<FlashList
|
||||||
|
ref={flashListRef}
|
||||||
data={tags}
|
data={tags}
|
||||||
estimatedItemSize={52}
|
estimatedItemSize={52}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
renderItem={({ item: tag }) => (
|
renderItem={({ item: tag }) => (
|
||||||
<TouchableRipple
|
<TouchableRipple
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
navigation.navigate(ROUTE.EDIT_TAG, { id: tag.id.toHexString() })
|
navigate(ROUTE.EDIT_TAG, { id: tag.id.toHexString() })
|
||||||
}>
|
}>
|
||||||
<View style={tagsStyles.tagRow}>
|
<View style={tagsStyles.tagRow}>
|
||||||
<View style={tagsStyles.tagView}>
|
<View style={tagsStyles.tagView}>
|
||||||
|
@@ -14,16 +14,16 @@ const styles = StyleSheet.create({
|
|||||||
paddingTop: '2%',
|
paddingTop: '2%',
|
||||||
},
|
},
|
||||||
padding: {
|
padding: {
|
||||||
padding: '5%',
|
padding: '4%',
|
||||||
},
|
},
|
||||||
paddingHorizontal: {
|
paddingHorizontal: {
|
||||||
paddingHorizontal: '5%',
|
paddingHorizontal: '4%',
|
||||||
},
|
},
|
||||||
paddingVertical: {
|
paddingVertical: {
|
||||||
paddingVertical: '5%',
|
paddingVertical: '4%',
|
||||||
},
|
},
|
||||||
paddingTop: {
|
paddingTop: {
|
||||||
paddingTop: '5%',
|
paddingTop: '4%',
|
||||||
},
|
},
|
||||||
centered: {
|
centered: {
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
|
Reference in New Issue
Block a user