React Native动画搜索栏高度调整:聚焦时扩展至半屏
解决React Native搜索栏Header高度变化内容层级问题
我在React Native中开发了一款类似Facebook移动端的动画搜索栏,点击头部的搜索按钮即可展开。当前搜索栏高度与所在的自定义Header高度一致,我希望实现当搜索栏获得焦点时,将Header高度扩展至屏幕的一半;当搜索栏失去焦点时,恢复Header的默认高度。但自行尝试实现时遇到问题:调整高度后,Header的内容会显示在搜索栏下方,这不符合预期,恳请提供解决方案。
问题根源
原有布局中Header的默认内容(Logo、标题、日期)与搜索栏为同级元素,未在搜索激活时隐藏;同时缺少Header高度的动画控制,导致高度变化后内容层级混乱。
实现方案
- 新增Header高度动画变量:控制Header在默认高度和屏幕一半高度间过渡
- 添加Header内容显隐动画:搜索激活时隐藏原有内容,避免重叠
- 同步所有动画时序:保证高度变化、内容隐藏、搜索栏滑入的动画同步执行
- 调整层级与布局:确保搜索栏始终在Header内容上方
修改后的完整代码
/* eslint-disable @typescript-eslint/no-unused-vars */ import React, {useContext, useRef} from 'react'; import { StyleSheet, Text, View, TouchableOpacity, Image, SafeAreaView, Animated, TextInput, Easing, } from 'react-native'; import Logo from '@assets/images/NCLogo.png'; import Colors from '@constants/colors/colors'; import Search from '@assets/icons/Search.svg'; import Back from '@assets/icons/back-arrow.svg'; import {AuthContext} from '@components/ContextStore/AuthContext/AuthContext'; import DateAndDayGenerator from '@utils/DayGen'; import {HEIGHT, WIDTH} from '@utils/Dimensions'; import Metrics from '@constants/metrics/Metrics'; import Fonts from '@constants/fonts/fonts'; import SearchList from '@components/ListNews/SearchList'; interface Props { news?: any; } const Header: React.FC<Props> = () => { const date = new Date(); const dateAndDay = DateAndDayGenerator(date); const {color} = useContext(AuthContext); const [focused, setFocused] = React.useState(false); const [value, setValue] = React.useState(''); const inputRef = useRef<TextInput>(null); // animation initial value const inputTranslateX = useRef(new Animated.Value(WIDTH)).current; const backButtonOpacity = useRef(new Animated.Value(0)).current; const contentTranslateY = useRef(new Animated.Value(HEIGHT)).current; const contentOpacity = useRef(new Animated.Value(0)).current; // 新增Header高度动画 const headerHeight = useRef(new Animated.Value(HEIGHT * 0.087)).current; // 新增Header内容显隐动画 const headerContentOpacity = useRef(new Animated.Value(1)).current; const handleClear = () => { setValue(''); }; const handleBlur = () => { setFocused(false); const inputTranslateXConfig = { toValue: WIDTH, duration: 200, useNativeDriver: true, easing: Easing.inOut(Easing.ease), }; const backButtonOpacityConfig = { toValue: 0, duration: 50, useNativeDriver: true, easing: Easing.inOut(Easing.ease), }; const contentTranslateYConfig = { toValue: HEIGHT, duration: 0, useNativeDriver: true, easing: Easing.inOut(Easing.ease), }; const contentOpacityConfig = { toValue: 0, duration: 200, useNativeDriver: true, easing: Easing.inOut(Easing.ease), }; // 恢复Header高度和内容显示 const headerHeightConfig = { toValue: HEIGHT * 0.087, duration: 200, useNativeDriver: false, // 高度变化不能用native driver easing: Easing.inOut(Easing.ease), }; const headerContentOpacityConfig = { toValue: 1, duration: 200, useNativeDriver: true, easing: Easing.inOut(Easing.ease), }; Animated.parallel([ Animated.timing(inputTranslateX, inputTranslateXConfig), Animated.timing(backButtonOpacity, backButtonOpacityConfig), Animated.timing(contentTranslateY, contentTranslateYConfig), Animated.timing(contentOpacity, contentOpacityConfig), Animated.timing(headerHeight, headerHeightConfig), Animated.timing(headerContentOpacity, headerContentOpacityConfig), ]).start(); inputRef.current?.blur(); }; const handleFocus = () => { setFocused(true); const inputTranslateXConfig = { toValue: 0, duration: 200, useNativeDriver: true, easing: Easing.inOut(Easing.ease), }; const backButtonOpacityConfig = { toValue: 1, duration: 200, useNativeDriver: true, easing: Easing.inOut(Easing.ease), }; const contentTranslateYConfig = { toValue: 0, duration: 0, useNativeDriver: true, easing: Easing.inOut(Easing.ease), }; const contentOpacityConfig = { toValue: 1, duration: 200, useNativeDriver: true, easing: Easing.inOut(Easing.ease), }; // 扩展Header高度并隐藏内容 const headerHeightConfig = { toValue: HEIGHT * 0.5, duration: 200, useNativeDriver: false, easing: Easing.inOut(Easing.ease), }; const headerContentOpacityConfig = { toValue: 0, duration: 200, useNativeDriver: true, easing: Easing.inOut(Easing.ease), }; Animated.parallel([ Animated.timing(inputTranslateX, inputTranslateXConfig), Animated.timing(backButtonOpacity, backButtonOpacityConfig), Animated.timing(contentTranslateY, contentTranslateYConfig), Animated.timing(contentOpacity, contentOpacityConfig), Animated.timing(headerHeight, headerHeightConfig), Animated.timing(headerContentOpacity, headerContentOpacityConfig), ]).start(); inputRef.current?.focus(); }; return ( <> <SafeAreaView style={styles.header_safe}> <Animated.View style={[styles.header, {height: headerHeight}]}> <View style={styles.view}> {/* 用Animated.View包裹原有Header内容,添加opacity动画 */} <Animated.View style={{opacity: headerContentOpacity, flexDirection: 'row', alignItems: 'center'}}> <View style={styles.Logo}> <Image source={Logo} style={styles.image} /> </View> {color ? ( <View style={styles.texts}> <Text style={[styles.text, {color}]}>Nepali Congress</Text> <Text style={styles.date}>{`${dateAndDay.day}, ${dateAndDay.month} ${dateAndDay.date},${' '}${dateAndDay.year}`}</Text> </View> ) : ( <View style={styles.texts}> <Text style={styles.text}>Nepali Congress</Text> <Text style={styles.date}>{`${dateAndDay.day}, ${dateAndDay.month} ${dateAndDay.date},${' '}${dateAndDay.year}`}</Text> </View> )} </Animated.View> <TouchableOpacity onPress={handleFocus} style={{opacity: headerContentOpacity}}> <View style={styles.search}> <Search width={25} height={25} fill="none" /> </View> </TouchableOpacity> <Animated.View style={[ styles.input, {transform: [{translateX: inputTranslateX}]}, ]}> <Animated.View style={{opacity: backButtonOpacity}}> <TouchableOpacity activeOpacity={1} style={styles.back_icon} onPress={handleBlur}> <Back width={28} height={28} fill="#000" /> </TouchableOpacity> </Animated.View> <TextInput ref={inputRef} placeholder="Enter title, description or date of news" placeholderTextColor="#000" clearButtonMode="while-editing" onFocus={handleFocus} value={value} onChangeText={text => { setValue(text); }} style={styles.input_text} /> <TouchableOpacity onPress={handleClear} style={styles.clearButton}> <Text style={styles.clear}>X</Text> </TouchableOpacity> </Animated.View> </View> </Animated.View> </SafeAreaView> <Animated.View style={[ styles.content, { opacity: contentOpacity, transform: [{translateY: contentTranslateY}], }, ]}> <SafeAreaView style={styles.content_safe}> <View style={styles.content_inner}> <View style={styles.separator} /> <SearchList searchPhrase={value} /> </View> </SafeAreaView> </Animated.View> </> ); }; const styles = StyleSheet.create({ header_safe: { zIndex: 1000, }, header: { paddingHorizontal: 16, backgroundColor: Colors.white, }, view: { flex: 1, overflow: 'hidden', alignItems: 'flex-start', // 改为flex-start,避免内容垂直居中导致高度变化时错位 flexDirection: 'row', justifyContent: 'space-between', position: 'relative', }, Logo: { margin: 10, alignItems: 'center', flexDirection: 'row', width: WIDTH * 0.15, height: HEIGHT * 0.055, }, image: { width: '100%', height: '100%', resizeMode: 'stretch', }, texts: { marginLeft: 4, marginRight: WIDTH * 0.155, }, text: { fontSize: Metrics.h3, color: Colors.black, fontFamily: Fonts.type.montBold, width: WIDTH * 0.5, }, date: { marginTop: 4, fontSize: Metrics.body7, color: Colors.black, fontFamily: 'Mont-Regular', }, search: { width: 35, height: 35, right: 20, borderRadius: 80, backgroundColor: '#f5f5f5', flexDirection: 'row', alignItems: 'center', justifyContent: 'center', }, input: { height: HEIGHT * 0.087, flexDirection: 'row', alignItems: 'center', position: 'absolute', top: 0, left: 0, backgroundColor: Colors.white, width: WIDTH - 32, zIndex: 10, // 确保搜索栏在Header内容上方 }, back_icon: { width: 40, height: 40, borderRadius: 40, flexDirection: 'row', justifyContent: 'center', alignItems: 'center', marginRight: 5, transform: [{rotate: '180deg'}], }, input_text: { flex: 1, height: 37, backgroundColor: '#e4e6e8', borderRadius: 80, paddingHorizontal: 16, fontSize: 13, color: Colors.black, }, input_text1: { flex: 1, height: 37, backgroundColor: '#e4e6e8', borderRadius: 80, paddingHorizontal: 16, fontSize: 13, }, content: { position: 'absolute', width: WIDTH, height: HEIGHT, left: 0, bottom: 0, zIndex: 999, }, content_safe: { flex: 1, backgroundColor: Colors.white, }, content_inner: { flex: 1, paddingTop: HEIGHT * 0.5, // 同步改为Header扩展后的高度,避免内容被遮挡 }, separator: { height: 1, marginTop: 5, backgroundColor: '#e6e4eb', }, search_results: { alignItems: 'center', borderBottomWidth: 1, borderBottomColor: '#e6e4eb', marginLeft: 16, }, clearButton: { width: 30, height: 30, borderRadius: 80, marginLeft: 10, backgroundColor: Colors.red, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', }, clear: { fontSize: 14, color: Colors.black, fontWeight: 'bold', }, }); export default Header;
关键修改点说明
- 新增
headerHeight动画变量:控制Header在默认高度和屏幕一半高度间平滑过渡,注意高度动画不能使用useNativeDriver - 新增
headerContentOpacity动画变量:搜索激活时隐藏原有Header内容,避免与搜索栏重叠 - 调整
styles.view的alignItems为flex-start:防止Header高度变化时内容垂直居中导致错位 - 给搜索栏添加
zIndex:10:确保搜索栏始终在原有Header内容上方 - 修改
content_inner的paddingTop为HEIGHT * 0.5:同步Header扩展后的高度,避免搜索结果被遮挡
内容的提问来源于stack exchange,提问作者niteshraj
相关产品推荐
相关产品推荐

