Skip to content

Pressable 组件

通过前面的学习,我们已经知道在 RN 中提供了 ButtonTouchable 这两个交互组件来处理用户的点击操作。但是到了 RN 0.63 版本,官方又提供了新的交互组件:Pressable

新的交互组件在未来将替代目前可以进行交互的组件:Button, TouchableWithoutFeedback, TouchableHighlight, TouchableOpacity, TouchableNativeFeedback

新核心组件 Pressable,可用于检测各种类型的交互。提供的 API 可以直接访问当前的交互状态,而不必在父组件中手动维护状态。它还可以使用各平台的所有功能,包括悬停,模糊,聚焦等。RN 希望开发者利用 Pressable 去设计组件,而不是使用带有默认效果的组件。如:TouchableOpacity

那么在这里,我们就要对这几代不同的交互组件做一个总结。

首先,开发者在开发时会用到点按组件,那么它的功能越简单开发者用起来就越轻松;但是与其相对的,应用最后开发出来是给用户使用的,对于用户来讲,则是希望功能越丰富就越能满足各种场景的需求。

那是让开发者简单易用好,还是用丰富的功能去满足用户,有没有两全其美之计?

实际上,RN 的点按组件经历了三个版本的迭代,才找到了两全其美的答案。等你了解了这个三个版本的迭代思路后,你就能很好明白优秀通用组件应该如何设计,才能同时在用户体验 UX 和开发者体验 DX 上找到平衡。

第一代 Touchable 组件#

是的,你没有看错,Touchable 系列组件反而是在 RN 中所提供的第一代点按组件。

第一代点按组件想要解决的核心问题是,提过多种反馈风格。

一个体验好的点按组件,需要在用户点按后进行实时地反馈,通过视觉变化等形式,告诉用户点到了什么,现在的点击状态又是什么。

但不同的原生平台,有不同的风格,反馈样式也不同。Android 按钮点击后会有涟漪,iOS 按钮点击后会降低透明度或者加深背景色。RN 是跨平台的,那它应该如何支持多种平台的多种反馈风格呢?

第一代 Touchable 点按组件的设计思路是,提供多种原生平台的反馈风格给开发者自己选择。所以我们看到整个 Touchable 是一套组件,让开发者自己选择。

不过,对于开发者来讲,有经验的开发者可能知道如何进行选择,但新手却要花上很长时间,去了解不同组件之间的区别。所以说,Touchable 点按组件在提供多样性的功能支持的同时,也带来了额外的学习成本。

为了降低学习成本,RN 团队又开发了第二代点按组件——Button

第二代 Button 组件#

第二代 Button 组件的实质是对 Touchable 组件的封装。在 Android 上是 TouchableNativeFeedback 组件,在 iOS 上是 TouchableOpacity 组件。

Button 组件的设计思想就是,别让开发者纠结选啥组件了,框架已经选好了,点按反馈的样式就和原生平台的自身风格保持统一就好了。

但是这仍然存在一个问题,那就是要让大多数开发者都选择同一个默认的 UI 样式真是太难了,萝卜白菜各有所爱。

另外,用户的审美也在慢慢地变化,涟漪风格也好,降低透明风格也好,背景高亮风格也好,或许几年后就不会再流行了。甚至连 Button 这个概念本身,都在慢慢地变化,现在的 App 中几乎只要是个图片或者文字都能点按,不再局限于只有四四方方的色块才能点按了。

第三代 Pressable 组件#

第三代 Pressable 点按组件,不再是 Touchable 组件的封装,而是一个全新重构的点按组件,它的反馈效果可由开发者自行配置。

下面我们就来看一下 Pressable 组件的相关知识。

Pressable 是一个核心组件的封装,它可以检测到任意子组件的不同阶段的按压交互情况。

<Pressable onPress={onPressFunction}>
<Text>I'm pressable!</Text>
</Pressable>

在被 Pressable 包装的元素上:

在按下 onPressIn 后,将会出现如下两种情况的一种:

  1. 用户移开手指,依次触发 onPressOutonPress 事件。
  2. 按压持续 500 毫秒以上,触发 onLongPress 事件。(onPressOut 在移开手后依旧会触发。)
image-20220609151529002

下面是针对这几个事件的演示示例:

import React from 'react'
import { Pressable, StyleSheet, Text, View } from 'react-native'
const App = () => {
function onPressHandle() {
console.log('onPressHandle')
}
function onPressOutHandle() {
console.log('onPressOutHandle')
}
function onPressInHandle() {
console.log('onPressInHandle')
}
function onLongPressHandle() {
console.log('onLongPressHandle')
}
return (
<View style={styles.container}>
<Pressable
onPress={onPressHandle}
onPressIn={onPressInHandle}
onPressOut={onPressOutHandle}
onLongPress={onLongPressHandle}
>
<Text style={{ textAlign: 'center' }}>Press Me</Text>
</Pressable>
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
},
})
export default App

在上面的示例中,当我们轻点按钮时,会依次触发 PressIn、Press、PressOut,而如果按住不放,则是先触发 PressIn500ms 后触发 LongPress,松开之后触发 PressOut

关于点按时的样式,也是可以自定义的。来看下面的示例:

import React from 'react'
import { Pressable, StyleSheet, Text, View } from 'react-native'
const App = () => {
return (
<View style={styles.container}>
<Pressable
style={({ pressed }) => {
if (pressed) {
return styles.pressdStyle
} else {
return styles.unPressdStyle
}
}}
>
{({ pressed }) => {
// 根据是否点按返回不同的子组件
if (pressed) {
return (
<Text
style={{ textAlign: 'center', color: 'white', lineHeight: 100 }}
>
Pressd
</Text>
)
} else {
return (
<Text style={{ textAlign: 'center', color: 'white' }}>
Press Me
</Text>
)
}
}}
</Pressable>
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
},
pressdStyle: {
backgroundColor: 'rgb(210, 230, 255)',
height: 100,
lineHeight: '100',
},
unPressdStyle: {
backgroundColor: '#ccc',
},
})
export default App

Pressable 组件有一个可触发区域 HitRect,默认情况下,可触发区域 HitRect 就是盒模型中的不透明的可见区域。你可以通过修改 hitSlop 的值,直接扩大可触发区域。

例如:

<Pressable hitSlop={{ top: 20, bottom: 20, left: 20, right: 20 }}>
...
</Pressable>

在上面的示例中,我们增加了 Pressable 组件的可点击区域,并且明确指定了 4 个边各自扩充多少。在老点不中、老勾不中的场景中,你可以在不改变布局的前提下,设置 Pressable 组件的可触发区域 HitSlop,让可点击区域多个 10 像素、20 像素,让用户的更容易点中。

另外,在 Pressable 组件中还有一个可保留区域 PressRect 的概念。

点按事件可保留区域的偏移量(Press Retention Offset)默认是 0,也就是说默认情况下可见区域就是可保留区域。你可以通过设置 pressRetentionOffset 属性,来扩大可保留区域 PressRect

举一个例子,当你在购物 App 点击购买按钮时,你已经点到购买按钮了,突然犹豫,开始进行心理博弈,想点又不想点。手指从按钮上挪开了,又挪了进去,然后又挪开了,如此反复。这时还要不要触发点击事件呢?要不要触发,其实是根据你手指松开的位置来判断的,如果你松手的位置在可保留区域内那就要触发,如果不是那就不触发。

image-20220609151555635

最后我们把官网的示例看一下:

import React, { useState } from 'react'
import { Pressable, StyleSheet, Text, View } from 'react-native'
const App = () => {
const [timesPressed, setTimesPressed] = useState(0)
let textLog = ''
if (timesPressed > 1) {
textLog = timesPressed + 'x onPress'
} else if (timesPressed > 0) {
textLog = 'onPress'
}
return (
<View style={styles.container}>
<Pressable
onPress={() => {
setTimesPressed((current) => current + 1)
}}
style={({ pressed }) => [
{
backgroundColor: pressed ? 'rgb(210, 230, 255)' : 'white',
},
styles.wrapperCustom,
]}
>
{({ pressed }) => (
<Text style={styles.text}>{pressed ? 'Pressed!' : 'Press Me'}</Text>
)}
</Pressable>
<View style={styles.logBox}>
<Text testID="pressable_press_console">{textLog}</Text>
</View>
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
},
text: {
fontSize: 16,
},
wrapperCustom: {
borderRadius: 8,
padding: 6,
},
logBox: {
padding: 20,
margin: 10,
borderWidth: StyleSheet.hairlineWidth,
borderColor: '#f0f0f0',
backgroundColor: '#f9f9f9',
},
})
export default App