-
Notifications
You must be signed in to change notification settings - Fork 941
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(discover): implement searchable header and enhance UI with anima…
…ted search bar - Added a new searchable header component to the Discover screen. - Integrated animated search input with focus handling and placeholder animations. - Updated the layout to include a safe navigation scroll view for better user experience. - Adjusted the FlashList component in the subscription list for automatic content inset adjustment. Signed-off-by: Innei <tukon479@gmail.com>
- v0.3.5
- v0.3.4-beta.0
- v0.3.3-beta.0
- v0.3.2-beta.0
- v0.3.1-beta.0
- v0.3.0-beta.0
- 0.3.4-nightly.20250213
- 0.3.3-nightly.20250212
- 0.3.3-nightly.20250211
- 0.3.3-nightly.20250210
- 0.3.3-nightly.20250209
- 0.3.3-nightly.20250208
- 0.3.3-nightly.20250207
- 0.3.3-nightly.20250206
- 0.3.3-nightly.20250205
- 0.3.3-nightly.20250204
- 0.3.3-nightly.20250203
- 0.3.3-nightly.20250201
- 0.3.3-nightly.20250131
- 0.3.3-nightly.20250130
- 0.3.3-nightly.20250129
- 0.3.3-nightly.20250128
- 0.3.3-nightly.20250127
- 0.3.3-nightly.20250126
- 0.3.3-nightly.20250125
- 0.3.2-nightly.20250123
- 0.3.2-nightly.20250122
- 0.3.2-nightly.20250121
- 0.3.2-nightly.20250120
- 0.3.2-nightly.20250119
- 0.3.2-nightly.20250118
- 0.3.2-nightly.20250117
- 0.3.1-nightly.20250116
- 0.3.1-nightly.20250115
- 0.3.1-nightly.20250114
- 0.3.1-nightly.20250113
- 0.3.1-nightly.20250112
- 0.3.1-nightly.20250111
- 0.3.1-nightly.20250110
- 0.3.1-nightly.20250109
- 0.3.0-nightly.20250108
- 0.3.0-nightly.20250107
- 0.3.0-nightly.20250106
- 0.3.0-nightly.20250105
- 0.3.0-nightly.20250104
- 0.3.0-nightly.20250103
- 0.3.0-nightly.20250102
- 0.2.9-nightly.20250101
- 0.2.9-nightly.20241231
- 0.2.9-nightly.20241230
- 0.2.9-nightly.20241229
- 0.2.9-nightly.20241228
- 0.2.9-nightly.20241227
Showing
3 changed files
with
235 additions
and
2 deletions.
There are no files selected for viewing
20 changes: 20 additions & 0 deletions
20
apps/mobile/src/components/common/SafeNavigationScrollView.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import { useBottomTabBarHeight } from "@react-navigation/bottom-tabs" | ||
import { useHeaderHeight } from "@react-navigation/elements" | ||
import type { FC } from "react" | ||
import type { ScrollViewProps } from "react-native" | ||
import { ScrollView, View } from "react-native" | ||
import { useSafeAreaInsets } from "react-native-safe-area-context" | ||
|
||
export const SafeNavigationScrollView: FC<ScrollViewProps> = ({ children, ...props }) => { | ||
const headerHeight = useHeaderHeight() | ||
const tabBarHeight = useBottomTabBarHeight() | ||
const insets = useSafeAreaInsets() | ||
|
||
return ( | ||
<ScrollView contentInsetAdjustmentBehavior="automatic" {...props}> | ||
<View style={{ height: headerHeight - insets.top }} /> | ||
<View>{children}</View> | ||
<View style={{ height: tabBarHeight - insets.bottom }} /> | ||
</ScrollView> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,217 @@ | ||
import { Text } from "react-native" | ||
import { getDefaultHeaderHeight } from "@react-navigation/elements" | ||
import { useTheme } from "@react-navigation/native" | ||
import { Stack } from "expo-router" | ||
import { atom, useAtom, useAtomValue, useSetAtom } from "jotai" | ||
import { useEffect, useRef } from "react" | ||
import { | ||
Animated, | ||
Easing, | ||
StyleSheet, | ||
Text, | ||
TextInput, | ||
TouchableOpacity, | ||
useAnimatedValue, | ||
View, | ||
} from "react-native" | ||
import { useSafeAreaFrame, useSafeAreaInsets } from "react-native-safe-area-context" | ||
|
||
import { HeaderBlur } from "@/src/components/common/HeaderBlur" | ||
import { SafeNavigationScrollView } from "@/src/components/common/SafeNavigationScrollView" | ||
import { Search2CuteReIcon } from "@/src/icons/search_2_cute_re" | ||
import { accentColor, useColor } from "@/src/theme/colors" | ||
|
||
export default function Discover() { | ||
return <Text>Discover</Text> | ||
return ( | ||
<SafeNavigationScrollView> | ||
<Stack.Screen | ||
options={{ | ||
headerShown: true, | ||
headerTransparent: true, | ||
|
||
header: SearchableHeader, | ||
}} | ||
/> | ||
|
||
<View className="bg-system-background"> | ||
<Text>11</Text> | ||
</View> | ||
</SafeNavigationScrollView> | ||
) | ||
} | ||
const SearchableHeader = () => { | ||
const frame = useSafeAreaFrame() | ||
const insets = useSafeAreaInsets() | ||
const headerHeight = getDefaultHeaderHeight(frame, false, insets.top) | ||
|
||
return ( | ||
<View style={{ height: headerHeight, paddingTop: insets.top }} className="relative"> | ||
<HeaderBlur /> | ||
<View style={styles.header}> | ||
<ComposeSearchBar /> | ||
</View> | ||
</View> | ||
) | ||
} | ||
|
||
const isFocusedAtom = atom(false) | ||
const searchValueAtom = atom("") | ||
const ComposeSearchBar = () => { | ||
const [isFocused, setIsFocused] = useAtom(isFocusedAtom) | ||
const setSearchValue = useSetAtom(searchValueAtom) | ||
return ( | ||
<> | ||
<SearchInput /> | ||
{isFocused && ( | ||
<TouchableOpacity | ||
hitSlop={10} | ||
onPress={() => { | ||
setIsFocused(false) | ||
setSearchValue("") | ||
}} | ||
> | ||
<Text className="ml-2 text-accent">Cancel</Text> | ||
</TouchableOpacity> | ||
)} | ||
</> | ||
) | ||
} | ||
|
||
const SearchInput = () => { | ||
const { colors } = useTheme() | ||
const [isFocused, setIsFocused] = useAtom(isFocusedAtom) | ||
const placeholderTextColor = useColor("placeholderText") | ||
const searchValue = useAtomValue(searchValueAtom) | ||
const setSearchValue = useSetAtom(searchValueAtom) | ||
const inputRef = useRef<TextInput>(null) | ||
|
||
const skeletonOpacityValue = useAnimatedValue(0) | ||
const skeletonTranslateXValue = useAnimatedValue(0) | ||
const placeholderOpacityValue = useAnimatedValue(1) | ||
|
||
useEffect(() => { | ||
if (isFocused) { | ||
Animated.timing(skeletonOpacityValue, { | ||
toValue: 0, | ||
duration: 100, | ||
easing: Easing.ease, | ||
useNativeDriver: true, | ||
}).start() | ||
Animated.timing(skeletonTranslateXValue, { | ||
toValue: -150, | ||
duration: 100, | ||
easing: Easing.inOut(Easing.ease), | ||
useNativeDriver: true, | ||
}).start() | ||
|
||
Animated.timing(placeholderOpacityValue, { | ||
toValue: 1, | ||
duration: 200, | ||
easing: Easing.ease, | ||
useNativeDriver: true, | ||
}).start() | ||
} else { | ||
Animated.timing(skeletonOpacityValue, { | ||
toValue: 1, | ||
duration: 100, | ||
easing: Easing.ease, | ||
useNativeDriver: true, | ||
}).start() | ||
|
||
Animated.timing(skeletonTranslateXValue, { | ||
toValue: 0, | ||
duration: 100, | ||
easing: Easing.inOut(Easing.ease), | ||
useNativeDriver: true, | ||
}).start() | ||
|
||
Animated.timing(placeholderOpacityValue, { | ||
toValue: 0, | ||
duration: 200, | ||
easing: Easing.ease, | ||
useNativeDriver: true, | ||
}).start() | ||
} | ||
}, [isFocused, skeletonOpacityValue, placeholderOpacityValue, skeletonTranslateXValue]) | ||
|
||
useEffect(() => { | ||
if (!isFocused) { | ||
inputRef.current?.blur() | ||
} | ||
}, [isFocused]) | ||
return ( | ||
<View style={{ backgroundColor: colors.card, ...styles.searchbar }}> | ||
{isFocused && ( | ||
<Animated.View | ||
style={{ | ||
opacity: placeholderOpacityValue, | ||
}} | ||
className="absolute inset-y-0 left-3 flex flex-row items-center justify-center" | ||
> | ||
<Search2CuteReIcon color={placeholderTextColor} height={18} width={18} /> | ||
{!searchValue && ( | ||
<Text className="text-placeholder-text ml-2" style={styles.searchPlaceholderText}> | ||
Search | ||
</Text> | ||
)} | ||
</Animated.View> | ||
)} | ||
<TextInput | ||
ref={inputRef} | ||
value={searchValue} | ||
cursorColor={accentColor} | ||
selectionColor={accentColor} | ||
style={styles.searchInput} | ||
onFocus={() => setIsFocused(true)} | ||
onBlur={() => setIsFocused(false)} | ||
onChangeText={(text) => setSearchValue(text)} | ||
/> | ||
|
||
<Animated.View | ||
style={{ | ||
opacity: skeletonOpacityValue, | ||
transform: [{ translateX: skeletonTranslateXValue }], | ||
}} | ||
className="absolute inset-0 flex flex-row items-center justify-center" | ||
pointerEvents="none" | ||
> | ||
<Search2CuteReIcon color={placeholderTextColor} height={18} width={18} /> | ||
<Text className="text-placeholder-text ml-1" style={styles.searchPlaceholderText}> | ||
Search | ||
</Text> | ||
</Animated.View> | ||
</View> | ||
) | ||
} | ||
const styles = StyleSheet.create({ | ||
header: { | ||
flex: 1, | ||
|
||
alignItems: "center", | ||
marginTop: -3, | ||
flexDirection: "row", | ||
marginBottom: 6, | ||
marginHorizontal: 16, | ||
position: "relative", | ||
}, | ||
searchbar: { | ||
flex: 1, | ||
display: "flex", | ||
flexDirection: "row", | ||
alignItems: "center", | ||
justifyContent: "center", | ||
|
||
borderRadius: 50, | ||
height: "100%", | ||
position: "relative", | ||
}, | ||
searchInput: { | ||
flex: 1, | ||
fontSize: 16, | ||
paddingRight: 16, | ||
paddingLeft: 35, | ||
height: "100%", | ||
}, | ||
searchPlaceholderText: { | ||
fontSize: 16, | ||
}, | ||
}) |