Мы можем легко реализовать сворачиваемый заголовок на Android с помощью CoordinatorLayout. Однако это не так просто, если мы хотим реализовать TabView со сворачиваемым заголовком с помощью чистого JavaScript на React Native. В настоящее время я написал пример на GitHub. Если вам интересно, здесь я дам краткое объяснение.

Посмотреть структуру

Как вы можете видеть выше, это представление состоит из двух частей. Во-первых, у нас есть представление заголовка с положением, установленным на absolute,, а второе - это компонент TabView с помощью response-native-tab-view. В TabView мы создаем компонент TabScene для визуализации Animated.FlatList для отображения списка на каждом маршруте вкладки. Вид будет выглядеть так:

<View style={{flex: 1}}> 
  {renderTabView()} 
  {renderHeader()} 
</View>

Сделать заголовок сворачиваемым

Чтобы сделать заголовок сворачиваемым, то есть при прокрутке любого списка вверх в TabView заголовок и TabBar также должны подниматься. Высота складного элемента должна быть такой же, как высота заголовка. Поэтому нам необходимо анимировать вертикальное положение обоих заголовков и TabBar вместе с прокруткой. Для этого мы объявляем scrollY для прослушивания onScroll события Animated.FlatList.

const scrollY = useRef(new Animated.Value(0)).current;
...
// inside the TabScene Component
<Animated.FlatList
  contentContainerStyle={{
    // TabScene should render below header and TabBar
    paddingTop: HeaderHeight + TabBarHeight,
    paddingHorizontal: 10,
  }}  
  onScroll={Animated.event(          
    [{nativeEvent: {contentOffset: {y: scrollY}}}],  
    {useNativeDriver: true},
  )}
...
/>

а для просмотра заголовка мы применяем translateY, интерполируя значение scrollY:

const renderHeader = () => {    
  const y = scrollY.interpolate({      
    inputRange: [0, HeaderHeight],
    outputRange: [0, -HeaderHeight],      
    extrapolateRight: 'clamp',    
  });
 return (
    <Animated.View style={[styles.header, 
      {transform: [{translateY: y}]}]}>
      // just a simple header
      <Text>{'Header'}</Text>      
    </Animated.View>    
 );  
};

затем для TabBar мы также должны применить translateY путем интерполяции значения scrollY :

const renderTabBar = (props) => {
  const y = scrollY.interpolate({
    inputRange: [0, HeaderHeight],
    outputRange: [HeaderHeight, 0],
  });
return(
  <Animated.View
    style={{
      ...
      transform: [{translateY: y}],
      position: 'absolute'
    }}>
    <TabBar
      ...
    />
  />

значение HeaderHeight зависит от высоты вашего заголовка. Одна из проблем заключается в том, что вы можете не знать фактическую высоту заголовка, поэтому вы можете использовать onLayout, чтобы установить фактическую высоту:

onLayout={({ nativeEvent }) => {
  setHeaderHeight(nativeEvent.layout.height);
}}

Теперь мы можем анимировать заголовок! но…

Проблема в том, что scrollOffset списка на другом маршруте все еще равно нулю. Событие прокрутки влияет только на текущий маршрут.

Синхронизация ScrollOffset для каждого маршрута

Чтобы решить указанную выше проблему, нам нужно поддерживать одно и то же scrollOffset в каждом маршруте. Идея здесь в том, чтобы знать, когда обновлять значение и какое значение мы должны обновлять.

Когда обновлять? В этом примере мы обновляем scrollOffset для двух обратных вызовов onMomentumScrollEnd и onScrollEndDrag.

Чтобы прокрутить до нужной позиции, мы сохраняем scrollOffset для каждого маршрута:

let listOffset = useRef({});
useEffect(() => {
  scrollY.addListener(({value}) => {
  const curRoute = routes[tabIndex].key;
  listOffset.current[curRoute] = value;
  });
  
  return () => {
    scrollY.removeAllListeners();
  };
}, [routes, tabIndex]);

тогда фактически мы здесь вызываем scrollToOffset по списку в других маршрутах, когда список в текущем маршруте перестает прокручиваться. Вы можете найти функцию под названием syncScrollOffset.

const syncScrollOffset = () => {
  const curRouteKey = routes[tabIndex].key;
  listRefArr.current.forEach((item) => {
    // sync value except current route
    if (item.key !== curRouteKey) {
      // if header has not yet been collapsed, scroll to current    
         offset
      if (scrollY._value < HeaderHeight && scrollY._value >= 0) {
        if (item.value) {
          item.value.scrollToOffset({
            offset: scrollY._value,
            animated: false,
          });
           // we should also update the offset here 
          listOffset.current[item.key] = scrollY._value;
        }
     // if header has been collapsed, scroll to HeaderHeight
     } else if (scrollY._value >= HeaderHeight) {
        if (listOffset.current[item.key] < HeaderHeight ||    
          listOffset.current[item.key] == null) {
           if (item.value) {
              item.value.scrollToOffset({
                offset: HeaderHeight,
                animated: false,
              });
              listOffset.current[item.key] = HeaderHeight;
            }
         }
      }
    }
  });
};

Вот и все. Спасибо за прочтение. Пожалуйста, дайте мне знать, если у вас есть какие-либо вопросы или комментарии. Более подробно о реализации читайте полный код на Github.