Add scroll-to-top on back

Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
2023-07-19 15:30:10 +03:00
parent 85732e247a
commit 1b2ce96c5e
6 changed files with 110 additions and 63 deletions

View 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;

View File

@@ -1,5 +1,6 @@
export { default as FloatingActionButton } from './floatingActionButton';
export { default as HideableBottomNavigationBar } from './hideableBottomNavigationBar';
export { default as HideableHeader } from './hideableHeader';
export { default as LoadingView } from './loadingView';
export { default as TagChip } from './tagChip';
export { default as TagPreview } from './tagPreview';
export { default as TagChip } from './tags/tagChip';
export { default as TagPreview } from './tags/tagPreview';

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { getContrastColor } from '../utilities';
import { getContrastColor } from '../../utilities';
import { Chip } from 'react-native-paper';
import { Tag } from '../database';
import { Tag } from '../../database';
import FontAwesome5 from 'react-native-vector-icons/FontAwesome5';
const TagChip = ({ tag }: { tag: Tag }) => {

View File

@@ -2,9 +2,9 @@ import React from 'react';
import { StyleSheet, View } from 'react-native';
import FontAwesome5 from 'react-native-vector-icons/FontAwesome5';
import { Chip } from 'react-native-paper';
import styles from '../styles';
import { useDimensions } from '../contexts';
import { getContrastColor } from '../utilities';
import styles from '../../styles';
import { useDimensions } from '../../contexts';
import { getContrastColor } from '../../utilities';
const tagPreviewStyles = StyleSheet.create({
chip: {

View File

@@ -1,11 +1,11 @@
import React, { useEffect, useRef, useState } from 'react';
import React, { useCallback, useRef, useState } from 'react';
import {
StyleSheet,
View,
Text,
NativeSyntheticEvent,
NativeScrollEvent,
Animated,
BackHandler,
} from 'react-native';
import {
Button,
@@ -19,7 +19,12 @@ import {
import { useQuery } from '@realm/react';
import { useDispatch, useSelector } from 'react-redux';
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 styles from '../styles';
import {
@@ -37,17 +42,8 @@ import {
tagSortQuery,
} from '../types';
import { getSortIcon } from '../utilities';
import { useDimensions } from '../contexts';
import { NavigationProp, useNavigation } from '@react-navigation/native';
const tagsStyles = StyleSheet.create({
headerView: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
zIndex: 100,
},
headerButtonView: {
height: 50,
},
@@ -74,8 +70,7 @@ const tagsStyles = StyleSheet.create({
const Tags = () => {
const { colors } = useTheme();
const { orientation } = useDimensions();
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const { navigate } = useNavigation<NavigationProp<RootStackParamList>>();
const sort = useSelector((state: RootState) => state.tags.sort);
const sortDirection = useSelector(
(state: RootState) => state.tags.sortDirection,
@@ -101,16 +96,6 @@ const Tags = () => {
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 tags = useQuery<Tag>(
@@ -132,17 +117,33 @@ const Tags = () => {
if (currentOffset <= 150) {
dispatch(setNavVisible(true));
setScrollOffset(0);
return;
} else {
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);
};
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 (
<View
style={[
@@ -150,26 +151,7 @@ const Tags = () => {
styles.fullSize,
{ backgroundColor: colors.background },
]}>
<Animated.View
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,
},
]}>
<HideableHeader visible={navVisisble}>
<Searchbar
placeholder="Search Tags"
value={search}
@@ -209,15 +191,16 @@ const Tags = () => {
</Menu>
</View>
<Divider />
</Animated.View>
</HideableHeader>
<FlashList
ref={flashListRef}
data={tags}
estimatedItemSize={52}
showsVerticalScrollIndicator={false}
renderItem={({ item: tag }) => (
<TouchableRipple
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.tagView}>

View File

@@ -14,16 +14,16 @@ const styles = StyleSheet.create({
paddingTop: '2%',
},
padding: {
padding: '5%',
padding: '4%',
},
paddingHorizontal: {
paddingHorizontal: '5%',
paddingHorizontal: '4%',
},
paddingVertical: {
paddingVertical: '5%',
paddingVertical: '4%',
},
paddingTop: {
paddingTop: '5%',
paddingTop: '4%',
},
centered: {
justifyContent: 'center',