You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React Native动画搜索栏高度调整:聚焦时扩展至半屏

解决React Native搜索栏Header高度变化内容层级问题

我在React Native中开发了一款类似Facebook移动端的动画搜索栏,点击头部的搜索按钮即可展开。当前搜索栏高度与所在的自定义Header高度一致,我希望实现当搜索栏获得焦点时,将Header高度扩展至屏幕的一半;当搜索栏失去焦点时,恢复Header的默认高度。但自行尝试实现时遇到问题:调整高度后,Header的内容会显示在搜索栏下方,这不符合预期,恳请提供解决方案。

问题根源

原有布局中Header的默认内容(Logo、标题、日期)与搜索栏为同级元素,未在搜索激活时隐藏;同时缺少Header高度的动画控制,导致高度变化后内容层级混乱。

实现方案

  1. 新增Header高度动画变量:控制Header在默认高度和屏幕一半高度间过渡
  2. 添加Header内容显隐动画:搜索激活时隐藏原有内容,避免重叠
  3. 同步所有动画时序:保证高度变化、内容隐藏、搜索栏滑入的动画同步执行
  4. 调整层级与布局:确保搜索栏始终在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.21 14:24:53