react / react/react-native

Horizontal ScrollView touchables stop responding when nested inside a sticky header (RN 0.79.2)

未关闭
#51,290 20 条评论 16 个 reaction 已指派 0 人 在 GitHub 查看

还没有人认领这个 Issue。

Component: ScrollView Needs: Triage :mag: Newer Patch Available
主要语言
C++
星标
127k
派生
25.3k
平均合并
1 天 23 小时
30 天内合并 PR
4

描述

Description

When you nest a horizontal ScrollView with touchable tabs inside a vertical ScrollView (or SectionList) that uses stickyHeaderIndices, the horizontal scroll still works, but the individual tab TouchableOpacity buttons no longer fire their onPress once the header becomes pinned. As soon as you scroll back up and the header unsticks, the buttons work again.


Steps to reproduce
  1. Create a fresh RN 0.79.2 project (or use your existing app).
  2. Copy this component into App.js (or similar):
import React, { useState } from 'react'
import { View, Text, ScrollView, TouchableOpacity, StyleSheet, SafeAreaView } from 'react-native'

const ItemBox = ({ title }) => (
  <View style={styles.itemBox}>
    <Text>{title}</Text>
  </View>
)

export default function NestedScrollTest() {
  const tabs = [
    { key: 'one', title: 'Tab One' },
    { key: 'two', title: 'Tab Two' },
    { key: 'three', title: 'Tab Three' },
    { key: 'four', title: 'Tab Four' },
    { key: 'five', title: 'Tab Five' },
  ]

  // Generate lots of items so it scrolls
  const allItems = Array.from({ length: 50 }, (_, i) => `Item #${i + 1}`)

  const [activeTab, setActiveTab] = useState(tabs[0].key)

  const filteredItems = allItems.filter(
    (_, idx) => idx % tabs.length === tabs.findIndex(t => t.key === activeTab)
  )

  return (
    <SafeAreaView style={styles.container}>
      <TouchableOpacity
        style={styles.submitButton}
        onPress={() => alert(`Submitting on "${activeTab}"`)}
      >
        <Text style={styles.submitText}>Top Submit Button</Text>
      </TouchableOpacity>
      {/* Vertical scroll area with sticky header */}
      <ScrollView
        style={styles.scrollContainer}
        contentContainerStyle={{ paddingBottom: 20 }}
        nestedScrollEnabled
        removeClippedSubviews={false}
        stickyHeaderIndices={[0]}
      >
        {/* Sticky horizontal tab bar */}
        <View style={styles.stickyContainer} pointerEvents="box-none">
          <ScrollView
            horizontal
            showsHorizontalScrollIndicator={false}
            nestedScrollEnabled
            pointerEvents="auto"
            contentContainerStyle={styles.tabBar}
            onStartShouldSetResponder={() => true}
          >
            {tabs.map(tab => (
              <TouchableOpacity
                key={tab.key}
                onPress={() => setActiveTab(tab.key)}
                activeOpacity={0.7}
                style={[
                  styles.tabButton,
                  activeTab === tab.key && styles.activeTab,
                ]}
              >
                <Text style={
                  activeTab === tab.key ? styles.activeTabText : styles.tabText
                }>
                  {tab.title}
                </Text>
              </TouchableOpacity>
            ))}
          </ScrollView>
        </View>

        {filteredItems.map(title => (
          <ItemBox key={title} title={title} />
        ))}
      </ScrollView>

      <TouchableOpacity
        style={styles.submitButton}
        onPress={() => alert(`Submitting on "${activeTab}"`)}
      >
        <Text style={styles.submitText}>Bottom Submit Button</Text>
      </TouchableOpacity>
    </SafeAreaView>
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  scrollContainer: {
    flex: 1,
  },
  stickyContainer: {
    backgroundColor: 'white',
    zIndex: 10,
    elevation: 4,
  },
  tabBar: {
    paddingVertical: 10,
    paddingHorizontal: 8,
  },
  tabButton: {
    paddingHorizontal: 16,
    paddingVertical: 8,
    marginRight: 8,
    borderRadius: 4,
    backgroundColor: '#EEE',
  },
  activeTab: {
    backgroundColor: '#4A90E2',
  },
  tabText: {
    fontSize: 14,
    color: '#333',
  },
  activeTabText: {
    fontSize: 14,
    color: '#FFF',
  },
  itemBox: {
    height: 80,
    marginHorizontal: 16,
    marginVertical: 8,
    backgroundColor: '#F9F9F9',
    borderRadius: 6,
    justifyContent: 'center',
    alignItems: 'center',
    elevation: 2,
  },
  submitButton: {
    padding: 16,
    backgroundColor: '#28A745',
    alignItems: 'center',
  },
  submitText: {
    color: '#FFF',
    fontSize: 16,
  },
})

  1. Run on Android.

  2. Scroll until the tab bar sticks to the top.

  3. Tap any tab button—nothing fires.

  4. Scroll up one pixel; tap again—the press now registers.

React Native Version

0.79.2

Affected Platforms

Runtime - Android

Output of npx @react-native-community/cli info
System:
  OS: Linux 6.8 Ubuntu 24.04.2 LTS 24.04.2 LTS (Noble Numbat)
  CPU: (6) x64 Intel(R) Core(TM) i5-9400F CPU @ 2.90GHz
  Memory: 12.45 GB / 23.38 GB
  Shell:
    version: 5.2.21
    path: /bin/bash
Binaries:
  Node:
    version: 22.11.0
    path: ~/.nvm/versions/node/v22.11.0/bin/node
  Yarn:
    version: 1.22.22
    path: ~/.nvm/versions/node/v22.11.0/bin/yarn
  npm:
    version: 10.9.0
    path: ~/.nvm/versions/node/v22.11.0/bin/npm

npmPackages:
  "@react-native-community/cli":
    installed: 18.0.0
    wanted: 18.0.0
  react:
    installed: 19.0.0
    wanted: 19.0.0
  react-native:
    installed: 0.79.2
    wanted: 0.79.2
MANDATORY Reproducer

https://github.com/Mootzali/NestedScrollIssue/

贡献指南

打开贡献指南

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

调研方向

从 Android 上必需的复现示例及其 App.js 组件开始,比较 header 固定和未固定时的标签页点击行为。跟踪 React Native Android 对嵌套水平 ScrollView 和 sticky header 的触摸处理,然后验证标签页的 TouchableOpacity 控件在固定状态下能够响应,同时不会破坏滚动。

由索引模型根据 Issue 内容生成。

评估

技术栈
react-native
领域
mobile
Issue 类型
缺陷
难度
4/5
预计耗时
3-5 天
活跃度
冷清
描述清晰度
基本清楚
新手友好度
48/100

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。