二十一、响应用户手势

在本书中,到目前为止您已经实现的所有示例都依赖于用户手势。在传统的 web 应用中,您通常担心鼠标事件。然而,触摸屏依靠用户的手指操纵元件,这与鼠标有根本的不同。

本章的目的是向您展示 React Native 内部的手势响应系统是如何工作的,以及该系统通过组件向我们展示的一些方式。

我们将从滚动开始。除了触摸之外,这可能是常见的手势。然后,我们将讨论在用户与我们的组件交互时,为他们提供适当级别的反馈。最后,我们将实现可以扫描的组件。

用手指滚动

web 应用中的滚动是通过使用鼠标指针来回或上下拖动滚动条,或旋转鼠标滚轮来完成的。这显然不适用于移动环境,因为没有鼠标。一切都由屏幕上的手势控制。例如,如果要向下滚动,可以使用拇指或食指在屏幕上移动手指,将内容向上拉。

这本身很难实现,但会变得更加复杂。在移动屏幕上滚动时,会考虑拖动运动的速度。快速拖动屏幕,然后放开,屏幕将根据移动速度继续滚动。您还可以在发生这种情况时触摸屏幕以阻止其滚动。

哎呀!谢天谢地,我们不必处理大部分这些事情。ScrollView组件为我们处理了大量的滚动复杂性。事实上,您已经在第 16 章中使用了ScrollView组件呈现项目列表ListView组件已经ScrollView烘焙到其中。

您可以通过实现手势生命周期方法来破解用户交互的低级部分。你可能永远都不需要这样做,但如果你感兴趣,你可以在上阅读 http://facebook.github.io/react-native/releases/next/docs/gesture-responder-system.html

您可以在ListView之外使用ScrollView。例如,如果您只是呈现任意内容,例如文本和其他小部件,而不是列表,那么您可以将其包装在一个<ScrollView>中。下面是一个例子:

import React from 'react'; 
import { 
  AppRegistry, 
  Text, 
  ScrollView, 
  ActivityIndicator, 
  Switch, 
  View, 
} from 'react-native'; 

import styles from './styles'; 

const FingerScrolling = () => ( 
  <View style={styles.container}> 
    { /* The "<ScrollView>" can wrap any 
         other component to make it scrollable. 
         Here, we're repeating an arbitrary group 
         of components to create some scrollable 
         content */ } 
    <ScrollView style={styles.scroll}> 
      {new Array(6).fill(null).map((v, i) => ( 
        <View key={i}> 
          {/* Arbitrary "<Text>" component... */} 
          <Text style={[styles.scrollItem, styles.text]}> 
            Some text 
          </Text> 

          {/* Arbitrary "<ActivityIndicator>"... */} 
          <ActivityIndicator 
            style={styles.scrollItem} 
            size="large" 
          /> 

          {/* Arbitrary "<Switch>" component... */} 
          <Switch style={styles.scrollItem} /> 
        </View> 
      ))} 
    </ScrollView> 
  </View> 
); 

AppRegistry.registerComponent( 
  'FingerScrolling', 
  () => FingerScrolling 
); 

ScrollView组件本身没有多大用处,它用于包装其他组件。但是,它确实需要一个高度才能正常工作。以下是滚动样式的外观:

scroll: { 
  height: 1, 
  alignSelf: 'stretch', 
}, 

height设置为1,但alignSelfstretch值允许项目正确显示。以下是最终结果:

Scrolling with our fingers

如您所见,当我向下拖动内容时,屏幕右侧有一个垂直滚动条。如果你运行这个例子,你可以做各种各样的手势,比如让内容自己滚动然后停止。

给予触摸反馈

到目前为止,您在本书中使用的 React 原生示例使用纯文本作为按钮或链接。在 web 应用中,很容易使文本看起来像是可以单击的内容—只需使用适当的链接将其包装即可。没有移动链接这样的东西,所以你可以把你的文本设计成一个按钮。

在移动设备上尝试将文本样式设置为链接的问题在于它们太难按。按钮为我的胖手指提供了一个更大的目标,它们更容易提供应用触摸反馈。

那么,让我们将一些文本样式设置为按钮。这是一个伟大的第一步,使文本看起来可触摸。但是,当用户开始与按钮交互时,我们还希望向用户提供视觉反馈。React Native 提供了两个组件来帮助实现这一点:TouchableOpacityTouchableHighlight。但是在深入研究代码之前,让我们看看用户在与它们交互时,视觉上这些组件的外观,首先从

Giving touch feedback

这里呈现了两个按钮,上面一个标记为不透明的按钮当前正被用户按下。按下按钮时,按钮的不透明度变暗,这为用户提供了重要的视觉反馈。让我们看看按下突出显示按钮时的样子:

Giving touch feedback

当按下按钮时,TouchableHighlight组件将在按钮上添加高光层,而不是改变不透明度。在本例中,我们将使用字体和边框颜色中使用的石板灰的更透明版本来突出显示它。

你使用哪种方法并不重要。重要的是,当用户与按钮交互时,您为他们提供适当的触摸反馈。事实上,您可能希望在同一个应用中使用这两种方法,但用途不同。让我们创建一个Button组件,使其易于使用任何一种方法:

import React, { PropTypes } from 'react'; 
import { 
  Text, 
  TouchableOpacity, 
  TouchableHighlight, 
} from 'react-native'; 

import styles from './styles'; 

// The "touchables" map is used to get the right 
// component to wrap around the button. The 
// "undefined" key represents the default. 
const touchables = new Map([ 
  ['opacity', TouchableOpacity], 
  ['highlight', TouchableHighlight], 
  [undefined, TouchableOpacity], 
]); 

const Button = ({ 
  label, 
  onPress, 
  touchable, 
}) => { 
  // Get's the "Touchable" component to use, 
  // based on the "touchable" property value. 
  const Touchable = touchables.get(touchable); 

  // Properties to pass to the "Touchable" 
  // component. 
  const touchableProps = { 
    style: styles.button, 
    underlayColor: 'rgba(112,128,144,0.3)', 
    onPress, 

  }; 

  // Renders the "<Text>" component that's 
  // styled to look like a button, and is 
  // wrapped in a "<Touchable>" component 
  // to properly handle user interactions. 
  return ( 
    <Touchable {...touchableProps}> 
      <Text style={styles.buttonText}> 
        {label} 
      </Text> 
    </Touchable> 
  ); 
}; 

Button.propTypes = { 
  onPress: PropTypes.func.isRequired, 
  label: PropTypes.string.isRequired, 
  touchable: PropTypes.oneOf([ 
    'opacity', 
    'highlight', 
  ]), 
}; 

export default Button; 

如您所见,touchables映射用于根据touchable属性值确定哪个 React Native touchable 组件包装文本。以下是用于创建此按钮的样式:

button: { 
  padding: 10, 
  margin: 5, 
  backgroundColor: 'azure', 
  borderWidth: 1, 
  borderRadius: 4, 
  borderColor: 'slategrey', 
}, 

buttonText: { 
  color: 'slategrey', 
} 

下面是我们如何在主应用模块中使用这些按钮:

import React from 'react'; 
import { 
  AppRegistry, 
  View, 
} from 'react-native'; 

import styles from './styles'; 
import Button from './Button'; 

const TouchFeedback = () => ( 
  <View style={styles.container}> 
    { /* Renders a "<Button>" that uses 
         "TouchableOpacity" to handle user 
         gestures, since that is the default */ } 
    <Button 
      onPress={() => {}} 
      label="Opacity" 
    /> 

    { /* Renders a "<Button>" that uses 
         "TouchableHighlight" to handle 
         user gestures. */ } 
    <Button 
      onPress={() => {}} 
      label="Highlight" 
      touchable="highlight" 
    /> 
  </View> 
); 

AppRegistry.registerComponent( 
  'TouchFeedback', 
  () => TouchFeedback 
); 

请注意,onPress回调实际上不做任何事情,但它们被调用,我们传递它们是因为它们是必需的属性。

可切换、可取消

使本机移动应用比移动 web 应用更易于使用的部分原因是它们感觉更直观。通过手势,你可以快速掌握事情的运作方式。例如,用手指在屏幕上滑动元素是一种常见的手势,但该手势必须是可发现的。

假设您正在使用一个应用,但您不确定屏幕上的某些内容是什么。因此,用手指向下按,然后尝试拖动该元素。它开始移动。不确定会发生什么,你抬起手指,元素就会移回原位。您刚刚了解了此应用的一部分是如何工作的。

我们将使用Scrollable组件实现如下可切换和可取消行为。我们可以创建一个有点通用的组件,允许用户从屏幕上滑动文本,当这种情况发生时,调用回调函数。但在查看通用组件本身之前,让我们先看一下呈现 swipeables 的代码:

import React, { Component } from 'react'; 
import { 
  AppRegistry, 
  View, 
} from 'react-native'; 
import { fromJS } from 'immutable'; 

import styles from './styles'; 
import Swipeable from './Swipeable'; 

class SwipableAndCancellable extends Component { 
  // The initial state is an immutable list of 
  // 8 swipable items. 
  state = { 
    data: fromJS(new Array(8) 
      .fill(null) 
      .map((v, id) => ({ id, name: 'Swipe Me' })) 
    ), 
  } 

  // Getter for "Immutable.js" state data... 
  get data() { 
    return this.state.data; 
  } 

  // Setter for "Immutable.js" state data... 
  set data(data) { 
    this.setState({ data }); 
  } 

  // The swipe handler passed to "<Swipeable>". 
  // The swiped item is removed from the state. 
  // This is a higher-order function that returns 
  // the real handler so that the "id" context 
  // can be set. 
  onSwipe = id => () => { 
    this.data = this.data 
      .filterNot(v => v.get('id') === id); 
  } 

  render() { 
    return ( 
      <View style={styles.container}> 
        {this.data.toJS().map(i => ( 
          <Swipeable 
            key={i.id} 
            onSwipe={this.onSwipe(i.id)} 
            name={i.name} 
          /> 
        ))} 
      </View> 
    ); 
  } 
} 

AppRegistry.registerComponent( 
  'SwipableAndCancellable', 
  () => SwipableAndCancellable 
); 

这将在屏幕上呈现八个<Swipeable>组件。让我们看看这是什么样子:

Swipeable and cancellable

现在,如果您开始向左滑动其中一个项目,它将移动。下面是它的样子:

Swipeable and cancellable

如果你刷得不够远,手势将被取消,物品将按预期移动回原位。如果一直滑动,则该项目将完全从列表中删除,屏幕上的项目将填充空白,如下所示:

Swipeable and cancellable

现在让我们看一下这个组件本身:

import React, { PropTypes } from 'react'; 
import { Map as ImmutableMap } from 'immutable'; 
import { 
  View, 
  ScrollView, 
  Text, 
  TouchableOpacity, 
} from 'react-native'; 

import styles from './styles'; 

// The "onScroll" handler. This is actually 
// a higher-order function that retuns the 
// actual handler. When the x offset is 200, 
// when know that the component has been 
// swiped and can call "onSwipe()". 
const onScroll = onSwipe => e => 
  ImmutableMap() 
    .set(200, onSwipe) 
    .get(e.nativeEvent.contentOffset.x, () => {}); 

// The static properties used by the "<ScrollView>" 
// component. 
const scrollProps = { 
  horizontal: true, 
  pagingEnabled: true, 
  showsHorizontalScrollIndicator: false, 
  scrollEventThrottle: 10, 
}; 

const Swipeable = ({ 
  onSwipe, 
  name, 
}) => ( 
  <View style={styles.swipeContainer}> 
    { /* The "<View>" that wraps this "<ScrollView>" 
         is necessary to make scrolling work properly. */ } 
    <ScrollView 
      {...scrollProps} 
      onScroll={onScroll(onSwipe)} 
    > 
      { /* Not strictly necessary, but "<TouchableOpacity>" 
           does provide the user with meaningful feedback 
           when they initially press down on the text. */ } 
      <TouchableOpacity> 
        <View style={styles.swipeItem}> 
          <Text style={styles.swipeItemText}>{name}</Text> 
        </View> 
      </TouchableOpacity> 
      <View style={styles.swipeBlank} /> 
    </ScrollView> 
  </View> 
); 

Swipeable.propTypes = { 
  onSwipe: PropTypes.func.isRequired, 
  name: PropTypes.string.isRequired, 
}; 

export default Swipeable; 

注意,<ScrollView>组件设置为水平,并且pagingEnabled为真。分页行为将组件捕捉到位,并提供可取消的行为。这就是为什么在包含文本的组件旁边有一个空白组件。以下是用于此组件的样式:

swipeContainer: { 
  flex: 1, 
  flexDirection: 'row', 
  width: 200, 
  height: 30, 
  marginTop: 50, 
}, 

swipeItem: { 
  width: 200, 
  height: 30, 
  backgroundColor: 'azure', 
  justifyContent: 'center', 
  borderWidth: 1, 
  borderRadius: 4, 
  borderColor: 'slategrey', 
}, 

swipeItemText: { 
  textAlign: 'center', 
  color: 'slategrey', 
}, 

swipeBlank: { 
  width: 200, 
  height: 30, 
}, 

swipeBlank样式的尺寸与swipeItem相同,但没有其他尺寸。它是隐形的。

总结

在本章中,您了解到,与移动 web 平台相比,本地平台上的手势是最大的区别。我们首先研究了ScrollView组件,以及它如何通过为包装的组件提供本机滚动行为来简化我们的生活。

接下来,我们花了一些时间实现带有触摸反馈的按钮。这是另一个在移动网络上很难把握的领域。您学习了如何使用TouchableOpacityTouchableHighlight组件。

最后,您实现了一个通用的Swipeable组件。刷卡是一种常见的移动模式,它允许用户在不感到害怕的情况下发现事物是如何工作的。在下一章中,您将学习如何使用 React Native 控制图像显示。