Add tag selector layout animations

Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
2023-07-26 18:31:32 +03:00
parent acba17462f
commit cecede4e28
8 changed files with 69 additions and 24 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "@karaolidis/terminally-online", "name": "@karaolidis/terminally-online",
"version": "0.0.1", "version": "0.0.2",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@karaolidis/terminally-online", "name": "@karaolidis/terminally-online",
"version": "0.0.1", "version": "0.0.2",
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@bankify/redux-persist-realm": "^0.1.3", "@bankify/redux-persist-realm": "^0.1.3",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@karaolidis/terminally-online", "name": "@karaolidis/terminally-online",
"version": "0.0.1", "version": "0.0.2",
"private": true, "private": true,
"scripts": { "scripts": {
"postinstall": "patch-package", "postinstall": "patch-package",

View File

@@ -1,5 +1,11 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { AppState, StatusBar, useColorScheme, StyleSheet } from 'react-native'; import {
AppState,
StatusBar,
useColorScheme,
StyleSheet,
UIManager,
} from 'react-native';
import { PaperProvider } from 'react-native-paper'; import { PaperProvider } from 'react-native-paper';
import { SafeAreaProvider } from 'react-native-safe-area-context'; import { SafeAreaProvider } from 'react-native-safe-area-context';
import { RealmProvider } from '@realm/react'; import { RealmProvider } from '@realm/react';
@@ -27,6 +33,7 @@ const App = () => {
const theme = isDarkMode ? darkTheme : lightTheme; const theme = isDarkMode ? darkTheme : lightTheme;
const onBeforeLift = async () => { const onBeforeLift = async () => {
UIManager.setLayoutAnimationEnabledExperimental(true);
await store.dispatch(validateSettings()); await store.dispatch(validateSettings());
const { settings } = store.getState(); const { settings } = store.getState();
if (!settings.storageUri) { if (!settings.storageUri) {

View File

@@ -2,7 +2,6 @@ export {
MemesList, MemesList,
MemeEditor, MemeEditor,
MemesHeader, MemesHeader,
MemeTagSearchModal,
MemeTagSelector, MemeTagSelector,
MemeViewItem, MemeViewItem,
} from './memes'; } from './memes';

View File

@@ -2,6 +2,5 @@ export { default as MemesList } from './memesList/memesList';
export { default as MemeEditor } from './memeEditor'; export { default as MemeEditor } from './memeEditor';
export { default as MemeFail } from './memeFail'; export { default as MemeFail } from './memeFail';
export { default as MemesHeader } from './memesHeader'; export { default as MemesHeader } from './memesHeader';
export { default as MemeTagSearchModal } from './memeTagSearchModal'; export { default as MemeTagSelector } from './memeTagSelector/memeTagSelector';
export { default as MemeTagSelector } from './memeTagSelector';
export { default as MemeViewItem } from './memeViewItem'; export { default as MemeViewItem } from './memeViewItem';

View File

@@ -41,7 +41,7 @@ const MemeEditor = ({
const { width } = useSafeAreaFrame(); const { width } = useSafeAreaFrame();
const { dimensions, loading, error } = useImageDimensions({ uri: memeUri }); const { dimensions, loading, error } = useImageDimensions({ uri: memeUri });
setMemeUriError(error); useEffect(() => setMemeUriError(error), [error, setMemeUriError]);
if (!memeUriError && (loading || !dimensions)) return <LoadingView />; if (!memeUriError && (loading || !dimensions)) return <LoadingView />;

View File

@@ -1,13 +1,13 @@
import React, { useEffect, useRef, useState } from 'react'; import React, { useRef, useState } from 'react';
import { useQuery, useRealm } from '@realm/react'; import { useQuery, useRealm } from '@realm/react';
import { Chip, Modal, Portal, Searchbar, useTheme } from 'react-native-paper'; import { Chip, Modal, Portal, Searchbar, useTheme } from 'react-native-paper';
import { StyleSheet } from 'react-native'; import { LayoutAnimation, StyleSheet } from 'react-native';
import { FlashList } from '@shopify/flash-list'; import { FlashList } from '@shopify/flash-list';
import { useSafeAreaFrame } from 'react-native-safe-area-context'; import { useSafeAreaFrame } from 'react-native-safe-area-context';
import { TAG_SORT, tagSortQuery } from '../../types'; import { TAG_SORT, tagSortQuery } from '../../../types';
import { TagChip } from '../tags'; import { TagChip } from '../../tags';
import { Tag } from '../../database'; import { Tag } from '../../../database';
import { validateTagName } from '../../utilities'; import { validateTagName } from '../../../utilities';
const memeTagSearchModalStyles = StyleSheet.create({ const memeTagSearchModalStyles = StyleSheet.create({
modal: { modal: {
@@ -26,6 +26,22 @@ const memeTagSearchModalStyles = StyleSheet.create({
}, },
}); });
const tagLayoutAnimation = {
duration: 150,
create: {
type: LayoutAnimation.Types.easeInEaseOut,
property: LayoutAnimation.Properties.opacity,
},
update: {
type: LayoutAnimation.Types.easeInEaseOut,
property: LayoutAnimation.Properties.opacity,
},
delete: {
type: LayoutAnimation.Types.easeInEaseOut,
property: LayoutAnimation.Properties.opacity,
},
};
const MemeTagSearchModal = ({ const MemeTagSearchModal = ({
visible, visible,
setVisible, setVisible,
@@ -46,13 +62,10 @@ const MemeTagSearchModal = ({
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [tagName, setTagName] = useState(validateTagName(search)); const [tagName, setTagName] = useState(validateTagName(search));
useEffect(() => {
setTagName(validateTagName(search));
}, [search]);
const handleSearch = (newSearch: string) => { const handleSearch = (newSearch: string) => {
flashListRef.current?.scrollToOffset({ offset: 0 }); flashListRef.current?.scrollToOffset({ offset: 0 });
setSearch(newSearch); setSearch(newSearch);
setTagName(validateTagName(newSearch));
}; };
const tags = useQuery<Tag>( const tags = useQuery<Tag>(
@@ -78,6 +91,8 @@ const MemeTagSearchModal = ({
const id = tag.id.toHexString(); const id = tag.id.toHexString();
memeTags.delete(id) || memeTags.set(id, tag); memeTags.delete(id) || memeTags.set(id, tag);
setMemeTags(new Map(memeTags)); setMemeTags(new Map(memeTags));
flashListRef.current?.prepareForLayoutAnimationRender();
LayoutAnimation.configureNext(tagLayoutAnimation);
}; };
const handleCreateTag = (name: string) => { const handleCreateTag = (name: string) => {
@@ -90,6 +105,8 @@ const MemeTagSearchModal = ({
if (!tag) return; if (!tag) return;
memeTags.set(tag.id.toHexString(), tag); memeTags.set(tag.id.toHexString(), tag);
setMemeTags(new Map(memeTags)); setMemeTags(new Map(memeTags));
flashListRef.current?.prepareForLayoutAnimationRender();
LayoutAnimation.configureNext(tagLayoutAnimation);
}; };
return ( return (
@@ -114,6 +131,7 @@ const MemeTagSearchModal = ({
ref={flashListRef} ref={flashListRef}
data={tags} data={tags}
extraData={memeTags} extraData={memeTags}
keyExtractor={tag => tag.id.toHexString()}
horizontal horizontal
estimatedItemSize={120} estimatedItemSize={120}
estimatedListSize={{ estimatedListSize={{

View File

@@ -1,11 +1,11 @@
import React, { ComponentProps, useState } from 'react'; import React, { ComponentProps, useRef, useState } from 'react';
import { StyleSheet, View } from 'react-native'; import { LayoutAnimation, StyleSheet, View } from 'react-native';
import { Chip } from 'react-native-paper'; import { Chip } from 'react-native-paper';
import { FlashList } from '@shopify/flash-list'; import { FlashList } from '@shopify/flash-list';
import { useSafeAreaFrame } from 'react-native-safe-area-context'; import { useSafeAreaFrame } from 'react-native-safe-area-context';
import { TagChip } from '../tags'; import { TagChip } from '../../tags';
import { Tag } from '../../database'; import { Tag } from '../../../database';
import { MemeTagSearchModal } from '.'; import MemeTagSearchModal from './memeTagSearchModal';
const memeTagSelectorStyles = StyleSheet.create({ const memeTagSelectorStyles = StyleSheet.create({
tagChip: { tagChip: {
@@ -13,6 +13,22 @@ const memeTagSelectorStyles = StyleSheet.create({
}, },
}); });
const tagLayoutAnimation = {
duration: 150,
create: {
type: LayoutAnimation.Types.easeInEaseOut,
property: LayoutAnimation.Properties.opacity,
},
update: {
type: LayoutAnimation.Types.easeInEaseOut,
property: LayoutAnimation.Properties.opacity,
},
delete: {
type: LayoutAnimation.Types.easeInEaseOut,
property: LayoutAnimation.Properties.opacity,
},
};
const MemeTagSelector = ({ const MemeTagSelector = ({
memeTags, memeTags,
setMemeTags, setMemeTags,
@@ -23,20 +39,26 @@ const MemeTagSelector = ({
} & ComponentProps<typeof View>) => { } & ComponentProps<typeof View>) => {
const { width } = useSafeAreaFrame(); const { width } = useSafeAreaFrame();
const flashListRef = useRef<FlashList<Tag>>(null);
const [flashListMargin, setFlashListMargin] = useState(0); const [flashListMargin, setFlashListMargin] = useState(0);
const [tagSearchModalVisible, setTagSearchModalVisible] = useState(false); const [tagSearchModalVisible, setTagSearchModalVisible] = useState(false);
const handleTagPress = (tag: Tag) => { const handleTagPress = (tag: Tag) => {
const id = tag.id.toHexString(); const id = tag.id.toHexString();
memeTags.delete(id) || memeTags.set(id, tag); memeTags.delete(id);
setMemeTags(new Map(memeTags)); setMemeTags(new Map(memeTags));
flashListRef.current?.prepareForLayoutAnimationRender();
LayoutAnimation.configureNext(tagLayoutAnimation);
}; };
return ( return (
<> <>
<View {...props}> <View {...props}>
<FlashList <FlashList
ref={flashListRef}
data={[...memeTags.values()]} data={[...memeTags.values()]}
keyExtractor={tag => tag.id.toHexString()}
horizontal horizontal
estimatedItemSize={120} estimatedItemSize={120}
showsHorizontalScrollIndicator={false} showsHorizontalScrollIndicator={false}