From 268c93e705e3d3808ab5353497354f390230d29d Mon Sep 17 00:00:00 2001 From: Leon Jiang <35908040+leonyjiang@users.noreply.github.com> Date: Sat, 3 Apr 2021 15:29:56 -0400 Subject: Fix search bar animation & search screen styles --- src/components/common/GradientBorderButton.tsx | 2 +- src/components/common/TabsGradient.tsx | 2 +- src/components/profile/PublicProfile.tsx | 2 +- src/components/search/RecentSearches.tsx | 31 ++-- src/components/search/SearchBar.tsx | 37 ++--- src/components/search/SearchCategories.tsx | 68 ++++---- src/components/search/SearchResultList.tsx | 85 +++++----- src/components/search/SearchResults.tsx | 14 +- src/components/search/SearchResultsBackground.tsx | 69 ++++++--- src/screens/search/DiscoverUsers.tsx | 2 +- src/screens/search/SearchScreen.tsx | 180 +++++++--------------- 11 files changed, 211 insertions(+), 281 deletions(-) (limited to 'src') diff --git a/src/components/common/GradientBorderButton.tsx b/src/components/common/GradientBorderButton.tsx index 32ac5c52..a5dbde9d 100644 --- a/src/components/common/GradientBorderButton.tsx +++ b/src/components/common/GradientBorderButton.tsx @@ -42,7 +42,7 @@ const GradientBorderButton: React.FC = ({ }; const styles = StyleSheet.create({ container: { - marginVertical: 15, + marginVertical: 10, }, gradientContainer: { width: SCREEN_WIDTH / 2 - 40, diff --git a/src/components/common/TabsGradient.tsx b/src/components/common/TabsGradient.tsx index a95e8bc3..07c55042 100644 --- a/src/components/common/TabsGradient.tsx +++ b/src/components/common/TabsGradient.tsx @@ -14,7 +14,7 @@ const TabsGradient: React.FC = () => { }; const styles = StyleSheet.create({ gradient: { - position: 'absolute', + ...StyleSheet.absoluteFillObject, top: (SCREEN_HEIGHT / 10) * 9, height: SCREEN_HEIGHT / 10, width: SCREEN_WIDTH, diff --git a/src/components/profile/PublicProfile.tsx b/src/components/profile/PublicProfile.tsx index 88e0ecd1..5f9b0b99 100644 --- a/src/components/profile/PublicProfile.tsx +++ b/src/components/profile/PublicProfile.tsx @@ -99,7 +99,7 @@ const PublicProfile: React.FC = ({ scrollViewRef.current ) { setScrollEnabled(false); - scrollViewRef.current.getNode().scrollTo({y: 0}); + scrollViewRef.current.scrollTo({y: 0}); navigation.navigate('MomentUploadPrompt', { screenType, momentCategory: momentCategories[0], diff --git a/src/components/search/RecentSearches.tsx b/src/components/search/RecentSearches.tsx index 84d35cac..6cea9338 100644 --- a/src/components/search/RecentSearches.tsx +++ b/src/components/search/RecentSearches.tsx @@ -6,6 +6,7 @@ import { StyleSheet, TouchableOpacityProps, ScrollView, + Keyboard, } from 'react-native'; import { PreviewType, @@ -15,7 +16,7 @@ import { } from '../../types'; import {TAGG_LIGHT_BLUE} from '../../constants'; import SearchResults from './SearchResults'; -import {SCREEN_HEIGHT} from '../../utils'; +import {useBottomTabBarHeight} from '@react-navigation/bottom-tabs'; interface RecentSearchesProps extends TouchableOpacityProps { sectionTitle: PreviewType; @@ -25,35 +26,25 @@ interface RecentSearchesProps extends TouchableOpacityProps { } const RecentSearches: React.FC = (props) => { - const {sectionTitle, recents, recentCategories, screenType} = props; + const {recents, recentCategories} = props; return ( - + <> - {sectionTitle} + Recent Clear all - - + + + + ); }; const styles = StyleSheet.create({ - mainContainer: { - flex: 1, - }, - contentContainer: { - paddingBottom: SCREEN_HEIGHT * 0.1, - flex: 1, - }, header: { paddingHorizontal: 25, paddingVertical: 5, diff --git a/src/components/search/SearchBar.tsx b/src/components/search/SearchBar.tsx index 7b833406..d441b07b 100644 --- a/src/components/search/SearchBar.tsx +++ b/src/components/search/SearchBar.tsx @@ -9,20 +9,23 @@ import { TextInputSubmitEditingEventData, TouchableOpacity, View, + ViewStyle, + LayoutChangeEvent, } from 'react-native'; import {normalize} from 'react-native-elements'; -import Animated, {interpolate} from 'react-native-reanimated'; +import Animated, {useAnimatedStyle} from 'react-native-reanimated'; import Icon from 'react-native-vector-icons/Feather'; import {useSelector} from 'react-redux'; import {RootState} from '../../store/rootReducer'; -import {getSearchSuggestions, SCREEN_HEIGHT} from '../../utils'; +import {getSearchSuggestions} from '../../utils'; const AnimatedIcon = Animated.createAnimatedComponent(Icon); interface SearchBarProps extends TextInputProps { onCancel: () => void; - top: Animated.Value; + animationProgress: Animated.SharedValue; searching: boolean; + onLayout: (e: LayoutChangeEvent) => void; } const SearchBar: React.FC = ({ onFocus, @@ -31,7 +34,8 @@ const SearchBar: React.FC = ({ value, onCancel, searching, - top, + animationProgress, + onLayout, }) => { const handleSubmit = ( e: NativeSyntheticEvent, @@ -107,23 +111,15 @@ const SearchBar: React.FC = ({ }, [searching]); /* - * Animated nodes used in search bar activation animation. + * On-search marginRight style ("cancel" button slides and fades in). */ - // TODO: (Leon) use reanimated v2 - const marginRight = 0; - // const marginRight: Animated.Node = interpolate(top, { - // inputRange: [-SCREEN_HEIGHT, 0], - // outputRange: [0, 58], - // }); - // TODO: (Leon) use reanimated v2 - const opacity = 0; - // const opacity: Animated.Node = interpolate(top, { - // inputRange: [-SCREEN_HEIGHT, 0], - // outputRange: [0, 1], - // }); + const animatedStyles = useAnimatedStyle(() => ({ + marginRight: animationProgress.value * 58, + opacity: animationProgress.value, + })); return ( - + = ({ style={[styles.input]} placeholderTextColor={'#828282'} onSubmitEditing={handleSubmit} - clearButtonMode="while-editing" + clearButtonMode="always" autoCapitalize="none" autoCorrect={false} {...{placeholder, value, onChangeText, onFocus, onBlur}} /> - + Cancel @@ -155,6 +151,7 @@ const styles = StyleSheet.create({ height: 40, paddingHorizontal: 20, flexDirection: 'row', + zIndex: 2, }, inputContainer: { flexGrow: 1, diff --git a/src/components/search/SearchCategories.tsx b/src/components/search/SearchCategories.tsx index c747b34f..3d142981 100644 --- a/src/components/search/SearchCategories.tsx +++ b/src/components/search/SearchCategories.tsx @@ -3,29 +3,40 @@ import React, {useEffect, useState} from 'react'; import {StyleSheet, View} from 'react-native'; import {getSuggestedSearchBubbleSuggestions} from '../../services/ExploreService'; import {SearchCategoryType} from '../../types'; -import {SCREEN_WIDTH} from '../../utils'; import GradientBorderButton from '../common/GradientBorderButton'; +import {useSelector} from 'react-redux'; +import {RootState} from 'src/store/rootReducer'; interface SearchCategoriesProps { darkStyle?: boolean; - defaultButtons?: SearchCategoryType[]; + useSuggestions: boolean; } const SearchCategories: React.FC = ({ darkStyle = false, - defaultButtons, + useSuggestions, }) => { const navigation = useNavigation(); - const mtSearchCategory: (key: number) => SearchCategoryType = (key) => ({ + const { + profile: {university = ''}, + } = useSelector((state: RootState) => state.user); + const defaultButtons: SearchCategoryType[] = [21, 22, 23, 24].map( + (year, index) => ({ + id: index * -1, + name: `${university.split(' ')[0]} '${year}`, + category: university, + }), + ); + const createloadingCategory: (key: number) => SearchCategoryType = (key) => ({ id: key, name: '...', category: '...', }); const [buttons, setButtons] = useState([ - mtSearchCategory(-1), - mtSearchCategory(-2), - mtSearchCategory(-3), - mtSearchCategory(-4), + createloadingCategory(1), + createloadingCategory(2), + createloadingCategory(3), + createloadingCategory(4), ]); useEffect(() => { @@ -36,7 +47,7 @@ const SearchCategories: React.FC = ({ setButtons(localButtons); } }; - if (!defaultButtons) { + if (useSuggestions) { loadButtons(); } else { setButtons(defaultButtons); @@ -45,33 +56,34 @@ const SearchCategories: React.FC = ({ return ( - {buttons.map((searchCategory) => ( - { - if (searchCategory.name !== '...') { - navigation.push('DiscoverUsers', { - searchCategory, - }); - } - }} - /> - ))} + + {buttons.map((searchCategory, index) => ( + { + if (searchCategory.name !== '...') { + navigation.push('DiscoverUsers', { + searchCategory, + }); + } + }} + /> + ))} + ); }; const styles = StyleSheet.create({ container: { - zIndex: 0, - top: '3%', - alignSelf: 'center', + paddingVertical: 20, + }, + categoryContainer: { flexDirection: 'row', - width: SCREEN_WIDTH * 0.9, - flexWrap: 'wrap', justifyContent: 'space-evenly', + flexWrap: 'wrap', }, }); export default SearchCategories; diff --git a/src/components/search/SearchResultList.tsx b/src/components/search/SearchResultList.tsx index 687b2285..a32760e1 100644 --- a/src/components/search/SearchResultList.tsx +++ b/src/components/search/SearchResultList.tsx @@ -1,15 +1,23 @@ import React, {useEffect, useState} from 'react'; -import {SectionList, StyleSheet, Text, View} from 'react-native'; +import { + SectionList, + StyleSheet, + Text, + View, + Keyboard, + SectionListData, +} from 'react-native'; import {useSelector} from 'react-redux'; import {RootState} from '../../store/rootreducer'; import {NO_RESULTS_FOUND} from '../../constants/strings'; import {PreviewType, ScreenType} from '../../types'; -import {normalize, SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils'; +import {normalize, SCREEN_WIDTH} from '../../utils'; import SearchResultsCell from './SearchResultCell'; +import {useBottomTabBarHeight} from '@react-navigation/bottom-tabs'; interface SearchResultsProps { - results: Array | undefined; - keyboardVisible: boolean; + // TODO: make sure results come in as same type, regardless of profile, category, badges + results: SectionListData[]; previewType: PreviewType; screenType: ScreenType; } @@ -21,11 +29,8 @@ const sectionHeader: React.FC = (showBorder: Boolean) => { return null; }; -const SearchResultList: React.FC = ({ - results, - keyboardVisible, -}) => { - const [showEmptyView, setshowEmptyView] = useState(false); +const SearchResultList: React.FC = ({results}) => { + const [showEmptyView, setshowEmptyView] = useState(false); const {user: loggedInUser} = useSelector((state: RootState) => state.user); useEffect(() => { @@ -38,57 +43,41 @@ const SearchResultList: React.FC = ({ } }, [results]); - return ( - - {showEmptyView && ( - - {NO_RESULTS_FOUND} - - )} - {!showEmptyView && ( - item.id + index} - renderItem={({section, item}) => { - return ( - - ); - }} - renderSectionHeader={({section: {data}}) => - sectionHeader(data.length !== 0) - } - /> - )} + return showEmptyView ? ( + + {NO_RESULTS_FOUND} + ) : ( + item.id + index} + renderItem={({item}) => { + return ( + + ); + }} + renderSectionHeader={({section: {data}}) => + sectionHeader(data.length !== 0) + } + stickySectionHeadersEnabled={false} + /> ); }; const styles = StyleSheet.create({ container: { - height: SCREEN_HEIGHT, - paddingBottom: SCREEN_HEIGHT * 0.1, - }, - sectionListContentContainer: { - paddingBottom: SCREEN_HEIGHT * 0.15, - width: SCREEN_WIDTH, + flex: 1, + marginTop: 30, + alignItems: 'center', }, sectionHeaderStyle: { width: '100%', height: 0.5, - marginBottom: normalize(24), + marginVertical: 5, backgroundColor: '#C4C4C4', }, - keyboardOpen: { - marginBottom: SCREEN_HEIGHT * 0.35, - }, noResultsTextContainer: { justifyContent: 'center', flexDirection: 'row', diff --git a/src/components/search/SearchResults.tsx b/src/components/search/SearchResults.tsx index ef518d8b..a73d0b40 100644 --- a/src/components/search/SearchResults.tsx +++ b/src/components/search/SearchResults.tsx @@ -1,18 +1,10 @@ import React from 'react'; -import { - ProfilePreviewType, - PreviewType, - ScreenType, - CategoryPreviewType, -} from '../../types'; -import {View} from 'react-native'; +import {ProfilePreviewType, CategoryPreviewType} from '../../types'; import SearchResultsCell from './SearchResultCell'; import {useSelector} from 'react-redux'; import {RootState} from '../../store/rootReducer'; interface SearchResultsProps { results: ProfilePreviewType[]; - previewType: PreviewType; - screenType: ScreenType; categories: CategoryPreviewType[]; } const SearchResults: React.FC = ({results, categories}) => { @@ -22,7 +14,7 @@ const SearchResults: React.FC = ({results, categories}) => { */ const {user: loggedInUser} = useSelector((state: RootState) => state.user); return ( - + <> {categories .slice(0) .reverse() @@ -43,7 +35,7 @@ const SearchResults: React.FC = ({results, categories}) => { {...{loggedInUser}} /> ))} - + ); }; diff --git a/src/components/search/SearchResultsBackground.tsx b/src/components/search/SearchResultsBackground.tsx index 3d7fab4e..e5236295 100644 --- a/src/components/search/SearchResultsBackground.tsx +++ b/src/components/search/SearchResultsBackground.tsx @@ -1,32 +1,55 @@ import React from 'react'; -import {StyleSheet} from 'react-native'; -import Animated, {interpolate} from 'react-native-reanimated'; -import {SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils'; +import {StyleSheet, ViewStyle} from 'react-native'; +import Animated, { + useAnimatedStyle, + useDerivedValue, + interpolate, + Extrapolate, +} from 'react-native-reanimated'; +import {useSafeAreaInsets} from 'react-native-safe-area-context'; interface SearchResultsBackgroundProps { - top: Animated.Value; + animationProgress: Animated.SharedValue; + searchBarHeight: number; + searching: boolean; } const SearchResultsBackground: React.FC = ({ - top, + animationProgress, + searchBarHeight, + searching, children, }) => { - // TODO: (Leon) use reanimated v2 - const opacityBackground = 0; - // const opacityBackground: Animated.Node = interpolate(top, { - // inputRange: [-SCREEN_HEIGHT, 0], - // outputRange: [0, 1], - // }); - // TODO: (Leon) use reanimated v2 - const opacityContent = 0; - // const opacityContent: Animated.Node = interpolate(top, { - // inputRange: [-SCREEN_HEIGHT / 40, 0], - // outputRange: [0, 1], - // }); + const {top: topInset} = useSafeAreaInsets(); + /* + * On-search container style (opacity fade-in). + */ + const backgroundAnimatedStyles = useAnimatedStyle(() => ({ + opacity: animationProgress.value, + })); + /* + * Derived animation value for contentAnimatedStyles. + */ + const contentAnimationProgress = useDerivedValue(() => + interpolate(animationProgress.value, [0.9, 1], [0, 1], Extrapolate.CLAMP), + ); + /* + * On-search content style (delayed opacity fade-in). + */ + const contentAnimatedStyles = useAnimatedStyle(() => ({ + opacity: contentAnimationProgress.value, + })); return ( - + style={[ + styles.container, + backgroundAnimatedStyles, + { + // absolute: inset + search screen paddingTop + searchBar + padding + paddingTop: topInset + 15 + searchBarHeight + 10, + }, + ]} + pointerEvents={searching ? 'auto' : 'none'}> + {children} @@ -34,15 +57,11 @@ const SearchResultsBackground: React.FC = ({ }; const styles = StyleSheet.create({ container: { - height: SCREEN_HEIGHT, - width: SCREEN_WIDTH, - position: 'absolute', + ...StyleSheet.absoluteFillObject, backgroundColor: 'white', }, contentContainer: { flex: 1, - paddingVertical: 10, - paddingBottom: SCREEN_HEIGHT / 15, }, }); export default SearchResultsBackground; diff --git a/src/screens/search/DiscoverUsers.tsx b/src/screens/search/DiscoverUsers.tsx index b87bfc37..f67585f2 100644 --- a/src/screens/search/DiscoverUsers.tsx +++ b/src/screens/search/DiscoverUsers.tsx @@ -126,7 +126,7 @@ const DiscoverUsers: React.FC = ({route}) => { ListFooterComponent={() => ( <> Other Groups - + )} /> diff --git a/src/screens/search/SearchScreen.tsx b/src/screens/search/SearchScreen.tsx index 4f0cabb4..f7e1c467 100644 --- a/src/screens/search/SearchScreen.tsx +++ b/src/screens/search/SearchScreen.tsx @@ -1,8 +1,19 @@ import AsyncStorage from '@react-native-community/async-storage'; import {useFocusEffect} from '@react-navigation/native'; import React, {useEffect, useState} from 'react'; -import {Keyboard, ScrollView, StatusBar, StyleSheet} from 'react-native'; -import Animated, {Easing, timing} from 'react-native-reanimated'; +import { + Keyboard, + StatusBar, + StyleSheet, + LayoutChangeEvent, + SectionListData, +} from 'react-native'; +import { + useSharedValue, + withTiming, + Easing, + runOnJS, +} from 'react-native-reanimated'; import {SafeAreaView} from 'react-native-safe-area-context'; import {useDispatch, useSelector} from 'react-redux'; import { @@ -13,22 +24,14 @@ import { SearchResultsBackground, TabsGradient, } from '../../components'; -import {SEARCH_ENDPOINT, TAGG_LIGHT_BLUE} from '../../constants'; +import {SEARCH_ENDPOINT} from '../../constants'; import {loadSearchResults} from '../../services'; import {resetScreenType} from '../../store/actions'; import {RootState} from '../../store/rootReducer'; -import { - CategoryPreviewType, - ProfilePreviewType, - ScreenType, - SearchCategoryType, -} from '../../types'; +import {CategoryPreviewType, ProfilePreviewType, ScreenType} from '../../types'; import { getRecentlySearchedCategories, getRecentlySearchedUsers, - normalize, - SCREEN_HEIGHT, - SCREEN_WIDTH, } from '../../utils'; /** @@ -38,11 +41,8 @@ import { const SearchScreen: React.FC = () => { const {recentSearches} = useSelector((state: RootState) => state.taggUsers); - const { - profile: {university = ''}, - } = useSelector((state: RootState) => state.user); const [query, setQuery] = useState(''); - const [results, setResults] = useState | undefined>(undefined); + const [results, setResults] = useState[] | undefined>(); const [recents, setRecents] = useState>( recentSearches ?? [], ); @@ -50,26 +50,12 @@ const SearchScreen: React.FC = () => { CategoryPreviewType[] >([]); const [searching, setSearching] = useState(false); - const top = Animated.useValue(-SCREEN_HEIGHT); - const defaultButtons: SearchCategoryType[] = [21, 22, 23, 24].map((year) => ({ - id: -1, - name: `${university.split(' ')[0]} '${year}`, - category: university, - })); - const [keyboardVisible, setKeyboardVisible] = React.useState( - 'keyboardVisible', - ); - useEffect(() => { - const showKeyboard = () => setKeyboardVisible('keyboardVisibleTrue'); - Keyboard.addListener('keyboardWillShow', showKeyboard); - return () => Keyboard.removeListener('keyboardWillShow', showKeyboard); - }, []); + /* + * Animated value + */ + const animationProgress = useSharedValue(0); + const [searchBarHeight, setSearchBarHeight] = useState(0); - useEffect(() => { - const hideKeyboard = () => setKeyboardVisible('keyboardVisibleFalse'); - Keyboard.addListener('keyboardWillHide', hideKeyboard); - return () => Keyboard.removeListener('keyboardWillHide', hideKeyboard); - }, []); const dispatch = useDispatch(); /* @@ -122,12 +108,22 @@ const SearchScreen: React.FC = () => { useEffect(() => { if (searching) { loadRecentlySearched().then(() => { - timing(top, topInConfig).start(); + animationProgress.value = withTiming(1, { + duration: 180, + easing: Easing.bezier(0.31, 0.14, 0.66, 0.82), + }); }); } else { setQuery(''); handleBlur(); - timing(top, topOutConfig).start(() => setResults(undefined)); + animationProgress.value = withTiming( + 0, + {duration: 180, easing: Easing.inOut(Easing.ease)}, + () => { + 'worklet'; + runOnJS(setResults)(undefined); + }, + ); } }, [searching]); @@ -153,16 +149,6 @@ const SearchScreen: React.FC = () => { } }; - const topInConfig = { - duration: 180, - toValue: 0, - easing: Easing.bezier(0.31, 0.14, 0.66, 0.82), - }; - const topOutConfig = { - duration: 180, - toValue: -SCREEN_HEIGHT, - easing: Easing.inOut(Easing.ease), - }; const handleFocus = () => { setSearching(true); }; @@ -172,9 +158,12 @@ const SearchScreen: React.FC = () => { const handleCancel = () => { setSearching(false); }; + const onSearchBarLayout = (e: LayoutChangeEvent) => { + setSearchBarHeight(e.nativeEvent.layout.height); + }; return ( - + { onBlur={handleBlur} onFocus={handleFocus} value={query} - {...{top, searching}} + onLayout={onSearchBarLayout} + {...{animationProgress, searching}} /> - - - - {results === undefined && - recents.length + recentCategories.length !== 0 ? ( + + + {results === undefined ? ( + recents.length + recentCategories.length > 0 && ( - ) : ( - - )} - - + ) + ) : ( + + )} + ); }; const styles = StyleSheet.create({ - screenContainer: { + container: { + flex: 1, paddingTop: 15, backgroundColor: '#fff', }, - contentContainer: { - height: SCREEN_HEIGHT, - paddingTop: '2%', - paddingBottom: SCREEN_HEIGHT / 3, - paddingHorizontal: '3%', - }, - header: { - marginVertical: 20, - zIndex: 1, - }, - recentsHeaderContainer: { - flexDirection: 'row', - }, - recentsHeader: { - fontSize: 17, - fontWeight: 'bold', - flexGrow: 1, - }, - clear: { - fontSize: normalize(17), - fontWeight: 'bold', - color: TAGG_LIGHT_BLUE, - }, - image: { - width: SCREEN_WIDTH, - height: SCREEN_WIDTH, - }, - textContainer: { - marginTop: '10%', - }, - headerText: { - color: '#fff', - fontSize: normalize(32), - fontWeight: '600', - textAlign: 'center', - marginBottom: '4%', - marginHorizontal: '10%', - }, - subtext: { - color: '#fff', - fontSize: normalize(16), - fontWeight: '600', - textAlign: 'center', - marginHorizontal: '10%', - }, - cancelButton: { - position: 'absolute', - height: '100%', - justifyContent: 'center', - paddingHorizontal: 5, - }, - cancelText: { - color: '#818181', - fontWeight: '600', - }, }); export default SearchScreen; -- cgit v1.2.3-70-g09d2