Skip to content

Commit

Permalink
feat(discover): implement searchable header and enhance UI with anima…
Browse files Browse the repository at this point in the history
…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>
  • Loading branch information
Innei committed Dec 27, 2024
1 parent 3e3a38e commit 2300996
Show file tree
Hide file tree
Showing 3 changed files with 235 additions and 2 deletions.
20 changes: 20 additions & 0 deletions apps/mobile/src/components/common/SafeNavigationScrollView.tsx
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>
)
}
1 change: 1 addition & 0 deletions apps/mobile/src/modules/subscription/list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ const RecycleList = ({ view }: { view: FeedViewType }) => {

return (
<FlashList
contentInsetAdjustmentBehavior="automatic"
scrollIndicatorInsets={{
bottom: tabHeight - insets.bottom,
top: headerHeight - insets.top + bottomViewTabHeight,
Expand Down
216 changes: 214 additions & 2 deletions apps/mobile/src/screens/(stack)/(tabs)/discover.tsx
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,
},
})

0 comments on commit 2300996

Please sign in to comment.