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 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';
|
||||
|
@@ -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 }) => {
|
@@ -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: {
|
@@ -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}>
|
||||
|
@@ -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',
|
||||
|
Reference in New Issue
Block a user