3.5 手势与触摸事件

传统的Web接口开发都是基于鼠标控制设计的,我们使用hover这样的状态来进行动态变换,并对状态的变换做出相应的响应处理。而对于移动设备而言,触控显得尤为重要。触控是移动设备的核心功能,也是移动应用交互的基础,Android和iOS都有各自完善的触摸事件处理机制。而在React Native开发中,React Native提供了一套统一的处理方式,能够方便地处理界面中组件的触摸事件、用户手势等。

3.5.1 触摸事件

React Native提供的组件中除了Text,其他组件默认是不支持点击事件的,也不能响应基本触摸事件,所以React Native提供了几个可以直接处理响应事件的组件,基本上能够满足大部分的点击处理需求。这些组件包括:TouchableHighlight、TouchableNativeFeedback、TouchableOpacity和TouchableWithoutFeedback。下面我们以TouchableHighlight为例进行介绍。

通常来讲,用户触摸任何界面元素响应结果都需要使用<TouchableHighlight>组件来包装,<TouchableHighlight>组件提供的主要响应事件包括:onPressIn、onPressOut、onPress、onLongPress等。例如,可以通过下面的代码来测试响应事件:

        onPressIn(){
            console.log("press in");
        }

        onPressOut(){
            console.log("press out");
        }

        onPress(){
            console.log("press");
        }

        onLonePress(){
            console.log("long press");
        }

        render(){
            return(
              <View style = {styles.container} >
                <TouchableHighlight
                style = {styles.touchable}
                onPressIn = {this._onPressIn}
                onPressOut = {this._onPressOut}
                onPress = {this._onPress}
                onLongPress = {this._onLonePress} >
                <View style = {styles.button} >
                </View>
                </TouchableHighlight>
                </View>
           );
        }

TouchableHighlight使用实例

下面是使用TouchableHighlight组件的Touch功能,实现点击文字从而弹出警告框的效果。如图3-11所示。

图3-11 TouchableHighlight点击弹窗

系统就会调用TouchableHighlight组件提供的onPress方法,将其放在段首位置,同时响应对应的事件,弹出警告框。

        //定义弹框
        show(){
                alert('React Native从入门到精通’);
            }
            //省略…
            <TouchableHighlight
                style={styles.container}
                onPress={()=>this.show()}>
                <Text style={styles.text}>React Native从入门到精通</Text>
              </TouchableHighlight>

除了TouchableHightLight组件实现了Touch功能之外,其他3个组件同样也实现了Touch功能,它们的主要功能和区别如下。

TouchableHightLight

本组件用于封装视图,用来正确响应触摸操作。当用户按下按钮的时候,封装的视图的透明度不会降低,同时底层颜色显示出来,使得视图变暗或变亮。如果使用的方法不恰当,有时候会导致一些不希望出现的视觉效果。

TouchableNativeFeedback(仅限于Android)

本组件用于封装视图,用来正确响应触摸操作(仅限Android平台)。在Android设备上,这个组件利用原生状态来渲染触摸的反馈,目前它只支持一个单独的View实例作为子节点。

TouchableOpacity

本组件用于封装视图,用来正确响应触摸操作。当用户按下按钮的时候,封装的视图的不透明度会降低。这个过程并不会真正改变视图层级,大部分情况下能很容易地添加到应用中而不会带来一些奇怪的副作用。此组件与TouchableHighlight的区别在于不会有额外的颜色变化。

TouchableWithoutFeedback

本组件用来封装视图,但是不会处理回调信息,一般不会使用这个组件。

在移动设备的交互设计中,除了简单的点击事件之外,往往还提供了更加复杂的手势响应系统,如多点触控、滑动等。

React Native的组件默认不处理触摸事件,要处理触摸事件,首先要向系统的“申请”成为触摸事件的响应器统一(Responder),当系统完成事件处理以后会释放响应器统一的角色。一个触摸事件处理周期,是从用户手指按下屏幕,到用户抬起手指结束。

3.5.2 手势系统响应

React Native也提供了两个API来处理手势触控逻辑:GestureResponder和PanResponder。其中GestureResponder是底层的一个接口,而PanResponder是内置的手势识别库,提供了很多实用的手势识别接口。要了解React Native的手势响应系统就必须先了解GestureResponder(手势响应)。

移动设备触摸操作背后的技术是相当复杂的,大多数移动设备都支持多点触控,这意味着在同一时刻系统需要处理多个有效的触摸点。当然多点触控有点复杂,所以系统提供了抽象的Touchable实现,通过实施正确的处理方法,视图可以成为接触响应器。前面讲过TouchableHightLight组件扮演响应器的角色,当然开发者也可以自己实现触摸响应器。如果一个视图想要变为响应器,需要实现以下几个方法:

· View.props.onStartShouldSetResponder

· View.props.onMoveShouldSetResponder

· View.props.onResponderGrant

· View.props.onResponderReject

为了更加形象地说明手势系统的工作流程,可以用如图3-12所示的流程图来说明。

图3-12 响应器工作流程

一般情况下,组件需要与Towchable组件配合才能实现触摸操作,一个正常的触摸响应事件流程应该是这样的:是否接受响应→响应触摸事件→释放触摸事件。在React Native的手势响应系统中,一个完整的触摸事件分为3个生命周期状态:开始(Start)、移动(move)和释放(release),对应Web浏览器的mouseDown、MouseMove和mouseUp 3个生命周期状态。具体来讲,一个视图可以在开始或者移动阶段请求成为触摸事件的响应器,这些动作通过onStartShouldSetResponder和onMoveShouldSetResponder来指定。当视图返回为true,说明视图申请响应触摸事件成功将进入相应的生命周期函数。

如果视图正在响应,那么会触发下列事件函数。

· View.props.onResponderMove:(evt)= > { }

用户正在移动手指。

· View.props.onResponderRelease:(evt)= > { }

在触摸结束被调用,即“touchUp”。

· View.props.onResponderTerminationRequest:(evt)= >true

其他元素想成为响应器,是否释放应答?返回true表示允许释放。

· View.props.onResponderTerminate:(evt)= > { }

响应器已经被回收。它可能被其他视图通过调用onResponderTerminationRequest之后被收回,也可能被系统强制收回(如iOS控制中心)。

有时候父组件想要成为响应器,就必须防止子视图成为应答器,所以在处理父组件时,onStartShouldSetResponderCapture返回true,视图就会尝试去得到应答器状态。当视图获得了应答器的状态后(可能失败,也可能成功)后,就会调用合适的回调函数,可能是onResponderGrant,也可能是onResponderReject,判断函数执行的方法叫做冒泡模式。

在冒泡模式下,最深的节点最先被调用,onStartShouldSetResponder和onMoveShould SetResponder函数将被最先调用,当多个视图在ShouldSetResponder处理程序返回true时,最深的组件会成为响应器。在大多数情况下,这种方式是可取的,因为它确保了所有控件和按钮是可用的。

除了上面的方法之外,系统还提供了一些额外的方法来对手势进行检测和响应。应答器是一个综合的响应事件,其他触摸状态如下。

· changedTouches

自从上个事件之后,所有发生改变的触摸事件的数组。

· dentifier

触摸的ID。

· locationX

触摸相对于元素的X位置。

· locationY

触摸相对于元素的Y位置。

· pageX

触摸相对于屏幕的X位置。

· pageY:

触摸相对于屏幕的Y位置。

· target:

接收触摸事件的元素的节点ID

· timestamp

触摸的时间标识符,用于速度计算。

· touches

所有当前在屏幕上触摸的数组。

PanResponder

手势响应系统中,除了上面提到的GestureResponder之外,还需要对PanResponder有一定的了解。PanResponder是React Native的一个类,它提供了处理原生相对高层的接口,用来处理更为复杂的触摸操作,例如多点触摸手势。对于每一个处理函数,它在原生事件之外提供了一个新的gestureState对象。gestureState对象里面的字段可以帮助开发者处理更加复杂的触摸状态。PanResponder提供的常见的触摸状态如下。

· stateID

触摸状态的ID,在屏幕上有至少一个触摸点的情况下,这个ID会一直有效。

· moveX

最近一次移动时的屏幕横坐标。

· moveY

最近一次移动时的屏幕纵坐标。

· x0

当响应器产生时的屏幕横坐标。

· y0

当响应器产生时的屏幕纵坐标。

· dx

从触摸操作开始时的累计横向路程。

· dy

从触摸操作开始时的累计纵向路程。

· vx

当前的横向移动速度。

· vy

当前的纵向移动速度。

· numberActiveTouches

当前在屏幕上的有效触摸点的数量。

为了在组件中使用PanResponder,我们需要创建一个PanResponder实例,然后将它添加到render的组件中。

先创建一个PanResponder实例。

          this.myPanResponder = PanResponder.create({
                //要求成为响应器统一:
                onStartShouldSetPanResponder:(evt, gestureState)=> true,
                onStartShouldSetPanResponderCapture:(evt, gestureState)=> true,
                onMoveShouldSetPanResponder:(evt, gestureState)=> true,
                onMoveShouldSetPanResponderCapture:(evt, gestureState)=> true,
                onPanResponderTerminationRequest:(evt, gestureState)=> true,
                //响应对应事件后的处理:
                onPanResponderGrant:(evt, gestureState)=> {
                  this.state.eventName=’触摸开始’;
                  this.forceUpdate();
                },
                onPanResponderMove:(evt, gestureState)=> {
                  var _pos = 'x:' + gestureState.moveX + ', y:' + gestureState.moveY;
                  this.setState({eventName:’移动’, pos : _pos});
                },
                onPanResponderRelease:(evt, gestureState)=> {
                  this.setState({eventName:’抬手’});
                },
                onPanResponderTerminate:(evt, gestureState)=> {
                  this.setState({eventName:’另一个组件已经成为了新的响应器统一’})
                },
            });

然后,将PanResponder添加到render方法的组件中。

            render(){
              return(
                <View style={styles.container} {...this.myPanResponder.panHandlers}>
                  <Text>eventName:{this.state.eventName}|{this.state.pos}</Text>
                </View>
             );
            }

之后,如果你的触摸起始于视图内,那么PanResponder.create的处理函数将会在相应的手势移动中被调用。

3.5.3 辅助功能

计算机辅助系统(Computer-aided system)是利用计算机辅助完成不同类任务的系统的总称。计算机辅助系统主要包含:计算机辅助设计(CAD)、计算机辅助制造(CAM)、计算机辅助工程(CAE)、计算机辅助测试(CAT)和计算机辅助教学(CAI)等。在手机等移动设备中,辅助系统也是不可缺少的一部分。

对Android系统而言,辅助功能涉及了许多不同的话题,其中之一是让丧失视力的人能够使用应用程序感知外部世界,谷歌提供了一个名叫TalkBack的内置屏幕读者服务机器人,借助该机器人,盲人可以通过触摸来使用移动设备和应用程序,TalkBack可以使用文本语音转换器来阅读屏幕上的内容,并且可以发出警报来通知用户有关应用程序中的重要信息。

而对iOS系统而言,辅助功能也涵盖了许多话题,但对大多数人来说辅助功能就是VoiceOver的代名词,即iOS 3.0版本推出的一种语音辅助程序。它充当屏幕阅读器的角色,帮助有视觉障碍的人更方便地使用iOS设备。

辅助功能属性

如果视图是辅助功能元素,它把它的子元素分组成一个单一的可选组件,默认情况下,可触摸的所有元素都具有辅助性。

在Android系统中,react-native视图中accessible={true}属性会被翻译成本地命令focusable={true}。例如:

        <View accessible={true}>
          <Text>text one</Text>
          <Text >text two</Text>
        </View>

在上面的例子中,当父视图开启无障碍属性后,将不能获得text one和text two的辅助焦点。但是,我们可以在父元素上使用accessible属性来获得焦点。

accessibilityLabel(Android、iOS)

如果要将视图标记为具有辅助性,那么一个比较好的做法就是为这个视图设置一个accessibilityLabel标签,以便让使用VoiceOver的人知道他们选择了什么元素。当用户选择某个元素之后,VoiceOver将会阅读响应的字符串文本。使用accessibilityLabel时,将视图中的accessibilityLabel属性设置为一个自定义的字符串即可。例如:

        <TouchableOpacity accessible={true} accessibilityLabel={'Tap me! '}
              onPress={this._onPress}>
          <View style={styles.button}>
            <Text style={styles.buttonText}>Press me! </Text>
          </View>
        </TouchableOpacity>

在上面的例子中,TouchableOpacity元素中的accessibilityLabel会被默认设置为“Press me! ”。该标签通过使用空格符来串联所有文本节点子元素。

accessibilityTraits (iOS)

辅助功能告诉用户在使用VoiceOver的时候选择了什么元素,以及相关的内容信息,accessibilityTraits常用的辅助字符串如下。

· none

当元素没有特征的时候使用。

· button

当元素需要被当作按钮的时候使用。

· link

当元素需要被当做链接的时候使用。

· header

当元素作为内容部分的标题(如导航栏中的标题)的时候使用。

· search

当文本字段元素被视为一个搜索字段的时候使用。

· image

当元素被作为图像的时候使用,可以和按钮或链接等连用。

· selected

当该元素被选中时使用。例如,表中被选中的行。

· plays

当元素被激活并且播放自己的声音的时候使用。

· key

当元素充当键盘按键的时候使用。

· text

当元素被视为不能更改的静态文本的时候使用。

· summary

当在应用程序首次启动的时候,该元素可以提供应用程序的实时状况的时候使用。例如,天气预报应用程序首次启动的时候,带有当天天气信息的元素将被该特征所标记。

· disabled

当控件未启动并且对用户的输入无响应的时候使用。

· frequentUpdates

当元素经常更新其标签或者值的时候,允许辅助功能客户端隔一段时间再去检查变化,避免频繁的更新操作。例如,秒表就是典型的例子。

· startsMedia

当激活元素并开始一段媒体会话(例如播放电影,录制音频)时,不会被辅助技术的输出所打断(如VoiceOver)。

· adjustable

当元素可以被“调整”的时候使用。

· allowsDirectInteraction

当元素允许VoiceOver用户直接进行触摸互动的时候使用。

· pageTurn

当它完成阅读的元素的内容需要通知VoiceOver滚动到下一个页面。

onAccessibilityTap (iOS)

此属性用来分配一个自定义的函数,当用户双击某个选中的元素时调用该函数。

onMagicTap (iOS)

当有人使用两个手指双击的时候,该属性就会被分配给一个自定义函数,同时,这个函数会被调用。在iPhone手机应用程序中,使用magic tap敲击可以接听或者结束一个电话。如果所选的元素不具有onMagicTap功能,该系统将遍历视图层次结构,直到它找到一个拥有此功能的视图为止。

accessibilityComponentType (Android)

在某些情况下,用户被提醒选定的组件类型(比如按钮),如果我们使用的是原生组件,这一行为会自动进行。由于我们使用JavaScript来开发应用程序,所以真正开发的时候就需要为Android环境下的TalkBack提供更多的使用环境。例如,让按钮支持button、radiobutton_checked和radiobutton_unchecked等属性,那么,就需要对UI组件的accessibilityComponentType属性做相关的声明。示例代码如下:

        <TouchableWithoutFeedback accessibilityComponentType="button"
          onPress={this._onPress}>
          <View style={styles.button}>
            <Text style={styles.buttonText}>Press me! </Text>
          </View>
        </TouchableWithoutFeedback>

accessibilityLiveRegion (Android)

当组件状态发生更改时,我们希望TalkBack能发出通知提醒用户,可以通过设置AccessibilityLiveRegion属性来实现这一诉求。它提供了诸如none、polite和assertive等属性。

· none

辅助功能服务不应该对此视图通知改变的地方。

· polite

辅助功能服务应该对此视图通知改变的地方。

· assertive

辅助功能服务应该中断正在进行的会话,并且以立即宣布该视图的改变。

        <TouchableWithoutFeedback onPress={this._addOne}>
          <View style={styles.embedded}>
            <Text>Click me</Text>
          </View>
        </TouchableWithoutFeedback>
        <Text accessibilityLiveRegion="polite">
          Clicked {this.state.count} times
        </Text>

在上面的例子中,通过监听_addOne的状态来监听用户的行为,当最终用户单击的时候,因为TalkBack设置了accessibilityLiveRegion="polite"属性,所以当它读取了文本视图中的文本后,就会发出视图改变的通知。

importantForAccessibility (Android)

有时候,面对两个重叠并且拥有相同父元素的组件,默认的辅助功能焦点行为往往是不可预知的,这时候就要通过ImportantForAccessibility属性来控制解决问题。importantForAccessibility提供的常用功能属性有auto、yes、no以及no-hide-descendants。示例代码如下:

        <View style={styles.container}>
          <View style={{backgroundColor: 'green'}} importantForAccessibility="yes">
            …
          </View>
          <View style={{backgroundColor: 'yellow'}}
              importantForAccessibility="no-hide-descendant">
            …
          </View>
        </View>