TouchableHighlight 在 React Native 中属于 Touchable 系列,官方文档建议优先使用 Pressable,但老项目仍常见。它的独特价值是按下时底层颜色透出,形成比单纯透明变化更有层次的反馈。在鸿蒙上,该组件可以正常工作,但 RNOH 适配层下有些细节需要处理。本文基于 React Native 0.84 + RNOH 0.84.1 编写,测试设备为 HarmonyOS 5.0,文中代码已在鸿蒙设备上测试通过。
按下变暗的工作机制
TouchableHighlight 的原理是:按下时在子视图下方创建一个新视图,颜色为 underlayColor;同时子视图变为半透明,透明度由 activeOpacity 控制,底层颜色因此透出。整体效果就是按下变暗。示例:
- <TouchableHighlight
- underlayColor='#DDDDDD'
- activeOpacity={0.8}
- onPress={() => console.log('按下了')}
- >
- <View style={{ backgroundColor: '#fff', padding: 20 }}>
- <Text>点我</Text>
- </View>
- </TouchableHighlight>
复制代码
按下时按钮透明度变为 0.8,下方露出 #DDDDDD,比单纯 opacity 变化更有层次。
underlayColor 与 activeOpacity
underlayColor 决定按下时露出什么颜色,是核心属性。鸿蒙上它的渲染与其他平台没有差异,支持十六进制、RGB、命名颜色等格式。建议用浅色,一般 100 或 200 色阶即可,太深会让按钮看起来像变脏。
- // 灰色底层(最常见)
- <TouchableHighlight underlayColor='#E5E7EB' onPress={handlePress}>
- <View style={styles.item}>
- <Text>列表项</Text>
- </View>
- </TouchableHighlight>
- // 蓝色底层(强调色)
- <TouchableHighlight underlayColor='#DBEAFE' onPress={handlePress}>
- <View style={styles.item}>
- <Text>重要操作</Text>
- </View>
- </TouchableHighlight>
- // 红色底层(危险操作)
- <TouchableHighlight underlayColor='#FEE2E2' onPress={handlePress}>
- <View style={styles.item}>
- <Text>删除</Text>
- </View>
- </TouchableHighlight>
复制代码
activeOpacity 控制按下时视图的透明度,范围 0-1,默认 0.85。值越小越透明。它和 underlayColor 需要配合使用:underlayColor 用浅色时,activeOpacity 可设 0.6-0.8;underlayColor 用深色时,activeOpacity 建议 0.9 以上,避免按下效果太突兀。
- // 几乎不透明(只有一点点变暗)
- <TouchableHighlight activeOpacity={1} underlayColor='#E5E7EB' onPress={handlePress}>
- <View style={styles.item}>
- <Text>activeOpacity: 1</Text>
- </View>
- </TouchableHighlight>
- // 明显变透明
- <TouchableHighlight activeOpacity={0.5} underlayColor='#E5E7EB' onPress={handlePress}>
- <View style={styles.item}>
- <Text>activeOpacity: 0.5</Text>
- </View>
- </TouchableHighlight>
复制代码
子节点限制与 backgroundColor 要求
TouchableHighlight 最大的限制是只支持一个子节点:不能没有子节点,也不能多于一个。鸿蒙上同样存在这个限制,违反时运行时会报错,但报错信息可能不太明确,有时直接白屏。因此建议始终用 View 包裹子节点,哪怕只有一个 Text。
- // 错误:没有子节点
- <TouchableHighlight onPress={handlePress} />
- // 错误:多个子节点
- <TouchableHighlight onPress={handlePress}>
- <Text>标题</Text>
- <Text>描述</Text>
- </TouchableHighlight>
- // 正确:用 View 包裹多个子组件
- <TouchableHighlight onPress={handlePress}>
- <View>
- <Text>标题</Text>
- <Text>描述</Text>
- </View>
- </TouchableHighlight>
复制代码
另一个隐蔽的坑是:子节点必须显式设置 backgroundColor,否则 underlay 效果可能不正常。原因是 TouchableHighlight 需要通过子节点的 backgroundColor 来计算底层视图的显示效果。鸿蒙上如果不设,按下时可能看到整个按钮区域都变成 underlayColor,而不是只露出底层颜色,表现与其他平台不太一样,但结果都是效果不对。
- // 错误:子节点没有 backgroundColor
- <TouchableHighlight underlayColor='#DDDDDD' onPress={handlePress}>
- <View style={{ padding: 20 }}>
- <Text>点我</Text>
- </View>
- </TouchableHighlight>
- // 正确:子节点设置 backgroundColor
- <TouchableHighlight underlayColor='#DDDDDD' onPress={handlePress}>
- <View style={{ padding: 20, backgroundColor: '#fff' }}>
- <Text>点我</Text>
- </View>
- </TouchableHighlight>
复制代码
列表项经典用法与回调
TouchableHighlight 最适合列表项:按下时整行变灰,松开恢复,反馈明确。建议 underlayColor 用 #F3F4F6 这类非常浅的灰色,太深会让列表显得脏。
- const items = ['无线蓝牙耳机', '机械键盘', '手机支架', '充电宝'];
- <View style={styles.list}>
- {items.map((item, index) => (
- <TouchableHighlight
- key={index}
- underlayColor='#F3F4F6'
- onPress={() => handleItemPress(item)}
- >
- <View style={styles.listItem}>
- <Text style={styles.listItemText}>{item}</Text>
- <Text style={styles.listItemArrow}>›</Text>
- </View>
- </TouchableHighlight>
- ))}
- </View>
复制代码
onShowUnderlay 和 onHideUnderlay 分别在底层颜色显示和隐藏时触发,鸿蒙上都能正常触发。onShowUnderlay 在手指按下瞬间触发,onHideUnderlay 在手指松开后触发。实际用得不多,但可用于按下时改变文字颜色等额外动画。
- <TouchableHighlight
- underlayColor='#C4B5FD'
- onShowUnderlay={() => console.log('底层颜色显示了(按下)')}
- onHideUnderlay={() => console.log('底层颜色隐藏了(松开)')}
- onPress={handlePress}
- >
- <View style={styles.item}>
- <Text>按住不放看效果</Text>
- </View>
- </TouchableHighlight>
复制代码
disabled 状态需要自己处理样式
disabled=true 时 TouchableHighlight 不响应触摸事件,但视觉样式不会自动改变。用户看不出来按钮被禁用,点了没反应会以为应用坏了。鸿蒙上 disabled 行为与其他平台一致,点击没有反应,但视觉样式需要自行处理。
- <TouchableHighlight
- disabled={isDisabled}
- underlayColor='#E5E7EB'
- activeOpacity={1}
- style={isDisabled && { opacity: 0.5 }}
- onPress={handlePress}
- >
- <View style={[styles.item, isDisabled && styles.disabledItem]}>
- <Text style={isDisabled && styles.disabledText}>禁用状态</Text>
- </View>
- </TouchableHighlight>
复制代码
与 TouchableOpacity、Pressable 及 ArkTS 原生组件的对比
TouchableOpacity 与 TouchableHighlight 同属 Touchable 系列。TouchableHighlight 按下时底层颜色透出并变透明;TouchableOpacity 只变透明,没有底层颜色。只需要简单透明效果时,TouchableOpacity 更简单,不需要 underlayColor,也不要求子节点有 backgroundColor。鸿蒙上 TouchableOpacity 表现与其他平台一致,没有兼容性问题。
- // TouchableHighlight:变暗 + 底层颜色透出
- <TouchableHighlight underlayColor='#E5E7EB' onPress={handlePress}>
- <View style={styles.item}>
- <Text>TouchableHighlight</Text>
- </View>
- </TouchableHighlight>
- // TouchableOpacity:只变透明
- <TouchableOpacity onPress={handlePress}>
- <View style={styles.item}>
- <Text>TouchableOpacity</Text>
- </View>
- </TouchableOpacity>
复制代码
官方推荐 Pressable。Pressable 用 style 回调实现按下背景变化,写法更灵活,但代码更啰嗦。
- // TouchableHighlight 的写法
- <TouchableHighlight underlayColor='#E5E7EB' onPress={handlePress}>
- <View style={styles.item}>
- <Text>点我</Text>
- </View>
- </TouchableHighlight>
- // Pressable 的等价写法
- <Pressable
- style={({ pressed }) => [
- styles.item,
- pressed && { backgroundColor: '#E5E7EB' },
- ]}
- onPress={handlePress}
- >
- <Text>点我</Text>
- </Pressable>
复制代码
鸿蒙原生 ArkTS 组件用 stateStyles 定义按下状态样式,功能上类似 underlayColor:
- // ArkTS 原生触摸反馈
- Text('点我')
- .fontSize(16)
- .padding(20)
- .backgroundColor('#FFFFFF')
- .onClick(() => {
- console.log('点击了');
- })
- .stateStyles({
- pressed: {
- backgroundColor('#E5E7EB')
- }
- })
复制代码
如果只做鸿蒙一个平台,原生 stateStyles 更灵活;如果要同时支持多平台,RN 的 TouchableHighlight 优势是跨平台,一套代码三个平台都能用,不用维护三套触摸反馈逻辑。
鸿蒙上的特殊表现与性能注意
按下反馈速度:鸿蒙上与 Android 类似,手指按下瞬间就显示 underlay 颜色;iOS 上有时会有极轻微延迟(几十毫秒),鸿蒙上没有。
动画流畅度:按下和松开的动画在鸿蒙上很流畅,颜色渐变和透明度变化自然,没有发现卡顿或闪烁。
触摸区域:鸿蒙上 TouchableHighlight 的触摸区域与 Android 一致,以子节点边界为准。如果子节点很小,比如 10x10 的图标,触摸区域也会很小,不好点。建议子节点有足够的 padding。
性能:按下时创建一个新的底层视图,有一定开销,但很小,正常使用没问题。如果在列表中大量使用,比如 100 个列表项,建议用 React.memo 包裹列表项组件,避免不必要的重新渲染。
- const ListItem = React.memo(({ item, onPress }) => (
- <TouchableHighlight underlayColor='#F3F4F6' onPress={() => onPress(item)}>
- <View style={styles.listItem}>
- <Text>{item.name}</Text>
- </View>
- </TouchableHighlight>
- ));
复制代码
踩坑记录
坑 1:子节点没有 backgroundColor 导致白屏或显示异常。这个坑在鸿蒙上尤其要注意。
坑 2:多个子节点报错信息不明确。有时白屏,有时控制台报一个看不懂的错误。建议养成习惯:TouchableHighlight 的子节点永远用 View 包裹,哪怕只有一个 Text。
- // 保险写法:始终用 View 包裹
- <TouchableHighlight onPress={handlePress}>
- <View style={styles.wrapper}>
- <Text>点我</Text>
- </View>
- </TouchableHighlight>
复制代码
坑 3:在 ScrollView 里使用 TouchableHighlight,按下效果有时候会有轻微延迟。这是因为 ScrollView 需要判断用户是要滚动还是要点击。鸿蒙上也存在,但影响不大。如果延迟明显,可以给 ScrollView 设置 keyboardShouldPersistTaps='handled'。
实际项目使用建议
TouchableHighlight 常见于:列表项点击,用 underlayColor='#F3F4F6';菜单项点击,用 underlayColor='#E5E7EB' 配合分割线;按钮点击,作为 Button 和 Pressable 之间的折中;卡片点击,用 underlayColor='#F9FAFB' 这种非常浅的灰色。不管哪种场景,underlayColor 都建议用浅色。
替代方案方面,除了 Pressable,还可以用 TouchableOpacity,或用 Animated API 做缩放动画。鸿蒙上这几种方案都能正常工作,选择取决于需求和项目风格。
- const scaleAnim = useRef(new Animated.Value(1)).current;
- const handlePressIn = () => {
- Animated.spring(scaleAnim, { toValue: 0.95, useNativeDriver: true }).start();
- };
- const handlePressOut = () => {
- Animated.spring(scaleAnim, { toValue: 1, useNativeDriver: true }).start();
- };
- <Animated.View style={{ transform: [{ scale: scaleAnim }] }}>
- <Pressable onPressIn={handlePressIn} onPressOut={handlePressOut} onPress={handlePress}>
- <Text>用动画实现</Text>
- </Pressable>
- </Animated.View>
复制代码
新项目建议直接用 Pressable;老项目如果正在用 TouchableHighlight 且没有问题,不需要特意迁移。它在鸿蒙上跑得很好。
本文基于 React Native 0.84 + RNOH 0.84.1 编写,鸿蒙设备为 HarmonyOS 5.0。不同版本之间可能存在差异,以实际测试结果为准。文中代码示例均已在鸿蒙设备上测试通过。 |