React Native 开发必备

638 阅读7分钟

这是我参与8月更文挑战的第6天,活动详情查看:8月更文挑战

触摸事件

移动应用上的用户交互基本靠“摸”。当然,“摸”也是有各种姿势的:在一个按钮上点击,在一个列表上滑动,或是在一个地图上缩放。

onPress

import { Button, Alert } from "react-native"

//...
<Button onPress={()=>{Alert.alert('受不了了~')}>click me</Button>

效果图:

image.png

点击 click me 就弹出弹框,按钮还可以添加color属性来修改按钮的颜色

Touchable 系列组件

这个组件的样式是固定的。如果要自定义就需要配合TouchableOpacityTouchableNativeFeedback来定制。

使用场景:

  • 可以使用TouchableHighlight来制作按钮或者链接。注意此组件的背景会在用户手指按下时变暗。
  • 在 Android 上还可以使用TouchableNativeFeedback,它会在用户手指按下时形成类似墨水涟漪的视觉效果。
  • TouchableOpacity会在用户手指按下时降低按钮的透明度,而不会改变背景的颜色。
  • 如果你想在处理点击事件的同时不显示任何视觉反馈,则需要使用TouchableWithoutFeedback*
import React, { Component } from 'react'
import {
    Button,
    Alert,
    Platform,
    StyleSheet,
    Text,
    TouchableHighlight,
    TouchableOpacity,
    TouchableNativeFeedback,
    TouchableWithoutFeedback,
    View
} from 'react-native'

export default class Touchables extends Component {
    _onPressButton() {
        Alert.alert('You tapped the button!')
    }

    _onLongPressButton() {
        Alert.alert('You long-pressed the button!')
    }

    render() {
        return (
            <View style={styles.container}>
                <Button
                    onPress={() => {
                        Alert.alert('受不了了~')
                    }}
                    title="click me!"
                />
                <TouchableHighlight
                    onPress={this._onPressButton}
                    underlayColor="white"
                >
                    <View style={styles.button}>
                        <Text style={styles.buttonText}>
                            TouchableHighlight
                        </Text>
                    </View>
                </TouchableHighlight>
                <TouchableOpacity onPress={this._onPressButton}>
                    <View style={styles.button}>
                        <Text style={styles.buttonText}>TouchableOpacity</Text>
                    </View>
                </TouchableOpacity>
                <TouchableNativeFeedback
                    onPress={this._onPressButton}
                    background={
                        Platform.OS === 'android'
                            ? TouchableNativeFeedback.SelectableBackground()
                            : ''
                    }
                >
                    <View style={styles.button}>
                        <Text style={styles.buttonText}>
                            TouchableNativeFeedback
                        </Text>
                    </View>
                </TouchableNativeFeedback>
                <TouchableWithoutFeedback onPress={this._onPressButton}>
                    <View style={styles.button}>
                        <Text style={styles.buttonText}>
                            TouchableWithoutFeedback
                        </Text>
                    </View>
                </TouchableWithoutFeedback>
                <TouchableHighlight
                    onPress={this._onPressButton}
                    onLongPress={this._onLongPressButton}
                    underlayColor="white"
                >
                    <View style={styles.button}>
                        <Text style={styles.buttonText}>
                            Touchable with Long Press
                        </Text>
                    </View>
                </TouchableHighlight>
            </View>
        )
    }
}

const styles = StyleSheet.create({
    container: {
        paddingTop: 60,
        alignItems: 'center'
    },
    button: {
        marginBottom: 30,
        width: 260,
        alignItems: 'center',
        backgroundColor: '#2196F3'
    },
    buttonText: {
        textAlign: 'center',
        padding: 20,
        color: 'white'
    }
})

效果图:

image.png

定时器

定时器是一个应用中非常重要的部分。React Native 实现了和浏览器一致的定时器。

API

  • setTimeout, clearTimeout
  • setInterval, clearInterval
  • setImmediate, clearImmediate
  • requestAnimationFrame, cancelAnimationFrame

requestAnimationFrame(fn)setTimeout(fn, 0)不同,前者会在每帧刷新之后执行一次,而后者则会尽可能快的执行(在 iPhone5S 上有可能每秒 1000 次以上)。

setImmediate则会在当前 JavaScript 执行块结束的时候执行,就在将要发送批量响应数据到原生之前。注意如果你在setImmediate的回调函数中又执行了setImmediate,它会紧接着立刻执行,而不会在调用之前等待原生代码。

Promise的实现就使用了setImmediate来执行异步调用。

InteractionManager

可以将一些耗时较长的工作安排到所有互动或动画完成之后再进行。这样可以保证JavaScript 动画的流畅运行。

与定时器方法对比:

  • requestAnimationFrame(): 用来执行在一段时间内控制视图动画的代码
  • setImmediate/setTimeout/setInterval(): 在稍后执行代码。注意这有可能会延迟当前正在进行的动画。
  • runAfterInteractions(): 在稍后执行代码,不会延迟当前进行的动画。

触摸处理系统会把一个或多个进行中的触摸操作认定为'交互',并且会将runAfterInteractions()的回调函数延迟执行,直到所有的触摸操作都结束或取消了。

InteractionManager 还允许应用注册动画,在动画开始时创建一个交互“句柄”,然后在结束的时候清除它。

const handle = InteractionManager.createInteractionHandle()
// 执行动画... (`runAfterInteractions`中的任务现在开始排队等候)
// 在动画完成之后
InteractionManager.clearInteractionHandle(handle)
// 在所有句柄都清除之后,现在开始依序执行队列中的任务

在组件卸载之后清除定时器可以规避一些致命性的错误;比如:闪退、长时间无响应。

手势响应系统

TouchableHighlight 与 Touchable 系列组件

响应系统用起来可能比较复杂。所以我们提供了一个抽象的Touchable实现,用来做“可触控”的组件。这一实现利用了响应系统,使得你可以简单地以声明的方式来配置触控处理。如果要做一个按钮或者网页链接,那么使用TouchableHighlight就可以。

响应者的生命周期

一个 View 只要实现了正确的协商方法,就可以成为触摸事件的响应者。我们通过两个方法去“询问”一个 View 是否愿意成为响应者:

  • View.props.onStartShouldSetResponder: (evt) => true, - 在用户开始触摸的时候(手指刚刚接触屏幕的瞬间),是否愿意成为响应者?
  • View.props.onMoveShouldSetResponder: (evt) => true, - 如果 View 不是响应者,那么在每一个触摸点开始移动(没有停下也没有离开屏幕)时再询问一次:是否愿意响应触摸交互呢?

如果 View 返回 true,并开始尝试成为响应者,那么会触发下列事件之一:

  • View.props.onResponderGrant: (evt) => {} - View 现在要开始响应触摸事件了。这也是需要做高亮的时候,使用户知道他到底点到了哪里。
  • View.props.onResponderReject: (evt) => {} - 响应者现在“另有其人”而且暂时不会“放权”,请另作安排。

如果 View 已经开始响应触摸事件了,那么下列这些处理函数会被一一调用:

  • View.props.onResponderMove: (evt) => {} - 用户正在屏幕上移动手指时(没有停下也没有离开屏幕)。
  • View.props.onResponderRelease: (evt) => {} - 触摸操作结束时触发,比如"touchUp"(手指抬起离开屏幕)。
  • View.props.onResponderTerminationRequest: (evt) => true - 有其他组件请求接替响应者,当前的 View 是否“放权”?返回 true 的话则释放响应者权力。
  • View.props.onResponderTerminate: (evt) => {} - 响应者权力已经交出。这可能是由于其他 View 通过onResponderTerminationRequest请求的,也可能是由操作系统强制夺权(比如 iOS 上的控制中心或是通知中心)。

evt是一个合成事件,它包含以下结构:

  • nativeEvent
    • changedTouches - 在上一次事件之后,所有发生变化的触摸事件的数组集合(即上一次事件后,所有移动过的触摸点)
    • identifier - 触摸点的 ID
    • locationX - 触摸点相对于当前元素的横坐标
    • locationY - 触摸点相对于当前元素的纵坐标
    • pageX - 触摸点相对于根元素的横坐标
    • pageY - 触摸点相对于根元素的纵坐标
    • target - 触摸点所在的元素 ID
    • timestamp - 触摸事件的时间戳,可用于移动速度的计算
    • touches - 当前屏幕上的所有触摸点的集合

捕获 ShouldSet 事件处理

onStartShouldSetResponderonMoveShouldSetResponder是以冒泡的形式调用的,即嵌套最深的节点最先调用。这意味着当多个 View 同时在*ShouldSetResponder中返回 true 时,最底层的 View 将优先“夺权”。在多数情况下这并没有什么问题,因为这样可以确保所有控件和按钮是可用的。

但是有些时候,某个父 View 会希望能先成为响应者。我们可以利用“捕获期”来解决这一需求。响应系统在从最底层的组件开始冒泡之前,会首先执行一个“捕获期”,在此期间会触发on*ShouldSetResponderCapture系列事件。因此,如果某个父 View 想要在触摸操作开始时阻止子组件成为响应者,那就应该处理onStartShouldSetResponderCapture事件并返回 true 值。

  • View.props.onStartShouldSetResponderCapture: (evt) => true,
  • View.props.onMoveShouldSetResponderCapture: (evt) => true,

网络连接

React Native 提供了和 web 标准一致的Fetch API,用于满足开发者访问网络的需求。当然也可以使用第三方库(axios 之类的)

Fetch

如果对 Fetch 不熟悉的话,可以先看看MDN 文档

使用 Fetch 来实现一个网络请求;要求:点击一个按钮发送请求,获取到数据后展示在页面上;

学习环节可以先用一些免费的在线 API 做测试,当然也可以自己搭建;推荐使用JSONPlacehodler,在线的、免费的、拿来即用的 RESTful 风格的接口。

代码如下:

import React, { Component } from 'react'
import {
    View,
    Text,
    Button,
    StyleSheet,
    FlatList,
    SafeAreaView,
    ActivityIndicator
} from 'react-native'
import Loading from '../../components/loading/Loading'

// 创建样式
const style = StyleSheet.create({
    text: {
        fontSize: 24,
        color: 'skyblue'
    },
    button: {
        width: '40%',
        height: 30,
        borderWidth: 1,
        borderColor: 'red'
    },
    item: {
        fontSize: 26,
        fontWeight: `500`,
        color: '#333',
        marginTop: 10
    },
    flatList: {
        borderColor: 'red',
        borderWidth: 1,
        width: '95%',
        height: '100%',
        marginLeft: 'auto',
        marginRight: 'auto'
    }
})

export default class LinFetch extends Component {
    state = {
        loading: false,
        count: 0,
        data: []
    }

    // 获取数据
    async handleFetch() {
        const result = await fetch(`https://jsonplaceholder.typicode.com/todos`)
        const data = await result.json()
        this.setState({ data, loading: false })
    }

    // 下拉加载
    handleOnRefresh() {
        this.setState({ loading: true })
        this.handleFetch()
    }

    // 上拉加载
    handleOnEndReached() {
        const { loading } = this.state
        this.setState({ loading: !loading })
    }

    render() {
        const { data, loading } = this.state
        return (
            <View style={{ flex: 1, width: '100%', height: '100%' }}>
                <Text style={style.text}>fetch request</Text>
                <Button
                    style={style.button}
                    onPress={() => this.handleFetch()}
                    title="request"
                />
                <SafeAreaView
                    style={{ flex: 1, width: '100%', height: '100%' }}
                >
                    <FlatList
                        style={style.flatList}
                        data={data}
                        refreshing={loading}
                        onRefresh={() => this.handleOnRefresh()}
                        onEndReached={() => this.handleOnEndReached()}
                        keyExtractor={item => item.id}
                        renderItem={({ item }) => (
                            <Text style={style.item}>{item.title}</Text>
                        )}
                        ListFooterComponent={loading && <Loading />}
                    />
                </SafeAreaView>
            </View>
        )
    }
}

在配置下拉刷新时,必须设置refreshing属性,否则将报如下图所示错误

image.png

最后效果图如下:

image.png

Axios

其实这些使用方法和开发 web 端应用时的方法如出一辙,如果还不熟悉 axios 可以去axios 官网熟悉熟悉。

  • 安装

    $ npm i axios
    or
    $ yarn add axios
    
  • 请求 JSONPlaceholder 中的图片

    state = {
        albumId: 1,
        data: [],
        isLoading: false,
    }
    
    async componentDidMount() {
        await this.handleGetData()
    }
    
    async handleGetData() {
        const { albumId, data } = this.state
        const result = await axios.get(
        		`https://jsonplaceholder.typicode.com/albums/${albumId}/photos`,
        )
        this.setState({
            data: [data, ...result.data],
            isLoading: false
        })
    }
    
  • 视图展示

    <FlatList
        data={data}
        renderItem={({ item }) => (
            <>
                <Image
                    style={styles.stretch}
                    source={{ uri: item.url }}
                    resizeMode="contain"
                />
                <Text>{item.title}</Text>
            </>
        )}
        keyExtractor={item => item.id}
        onEndReached={() => this.handleOnEndReached()}
    />
    

    image.png ​ 完整代码如下:

import React, { Component } from 'react'
import axios from 'axios'
import { View, Text, Image, StyleSheet, FlatList } from 'react-native'

const styles = StyleSheet.create({
    text: {
        fontSize: 24,
        fontWeight: '600'
    },
    stretch: {
        width: '100%',
        height: 500,
        resizeMode: 'stretch'
    }
})

class LinAxios extends Component {
    state = {
        albumId: 1,
        data: [],
        isLoading: false
    }

    componentDidMount() {
        this.handleGetData()
    }

    // 获取数据
    async handleGetData() {
        const { albumId, data } = this.state
        const result = await axios.get(
            `https://jsonplaceholder.typicode.com/albums/${albumId}/photos`
        )
        console.log('albmId:', albumId)
        this.setState({ data: [data, ...result.data], isLoading: false })
    }

    // 上拉加载
    async handleOnEndReached() {
        if (this.state.isLoading) return
        await this.setState({
            albumId: this.state.albumId + 1,
            isLoading: true
        })
        this.handleGetData()
    }

    render() {
        const { data } = this.state

        return (
            <View>
                <Text style={styles.text}>axios request</Text>
                <FlatList
                    data={data}
                    renderItem={({ item }) => (
                        <>
                            <Image
                                style={styles.stretch}
                                source={{ uri: item.url }}
                                resizeMode="contain"
                            />
                            <Text>{item.title}</Text>
                        </>
                    )}
                    keyExtractor={item => item.id}
                    onEndReached={() => this.handleOnEndReached()}
                />
            </View>
        )
    }
}

export default LinAxios