RN常用的APIs

243 阅读18分钟

1. 基础 API

从本章开始我们进入 RN 中一些 API 的学习。这里我将整个 RNAPI 分为了 5 个部分,分别是:

  • 基础 API
  • 屏幕 API
  • 动画 API
  • 设备 API
  • 手势 API

本小节我们先来看一下一些比较基础的 API,包含以下内容:

  • Alert
  • StyleSheet
  • Transforms
  • Keyboard
  • AppState

Alert

Alert 主要用于显示一个带有指定标题和消息的警报对话框。Alert.alert 方法接收 3 个参数,一个参数是警报对话框的标题,第二个参数是警报内容,最后一个参数是一个数组,数组的每一项是按钮对象。

import React, { useState } from "react";
import { View, StyleSheet, Button, Alert } from "react-native";

const App = () => {
  const createTwoButtonAlert = () =>
    Alert.alert(
      "Alert Title",
      "My Alert Msg",
      [
        {
          text: "Cancel",
          onPress: () => console.log("Cancel Pressed"),
          style: "cancel"
        },
        { text: "OK", onPress: () => console.log("OK Pressed") }
      ]
    );

  const createThreeButtonAlert = () =>
    Alert.alert(
      "Alert Title",
      "My Alert Msg",
      [
        {
          text: "Ask me later",
          onPress: () => console.log("Ask me later pressed")
        },
        {
          text: "Cancel",
          onPress: () => console.log("Cancel Pressed"),
          style: "cancel"
        },
        { text: "OK", onPress: () => console.log("OK Pressed") }
      ]
    );

  return (
    <View style={styles.container}>
      <Button title={"2-Button Alert"} onPress={createTwoButtonAlert} />
      <Button title={"3-Button Alert"} onPress={createThreeButtonAlert} />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "space-around",
    alignItems: "center"
  }
});

export default App;

官方 API 文档:reactnative.dev/docs/alert

StyleSheet

这个 API 我们在前面已经用过很多次了,StyleSheet 是一种类似于 CSS StyleSheets 的抽象。

需要注意以下几个点:

  • 并不是所有的 CSS 属性在 StyleSheet 中都支持
  • 书写样式时要使用驼峰命名法,例如 backgroundColor

属性:

  • hairlineWidth: 自适应不同设备生成一条线
var styles = StyleSheet.create({
  separator: {
    borderBottomColor: '#bbb',
    borderBottomWidth: StyleSheet.hairlineWidth,
  },
});
  • absoluteFill: (position: 'absolute', left: 0, right: 0, top: 0, bottom: 0) 的缩写形式
const styles = StyleSheet.create({
  wrapper: {
    ...StyleSheet.absoluteFill,
    top: 10,
    backgroundColor: 'transparent',
  },
});

相当于以下代码的缩写:

position: 'absolute',
left: 0, 
right: 0, 
top: 0, 
bottom: 0

方法:

  • create: 根据对象创建样式表
  • flatten: 可以把样式对象的数组整合成一个样式对象,重复的样式属性以后一个为准
var styles = StyleSheet.create({
  listItem: {
    flex: 1,
    fontSize: 16,
    color: 'white',
  },
  selectedListItem: {
    color: 'green',
  },
});

console.log(StyleSheet.flatten([styles.listItem, styles.selectedListItem]));
// returns { flex: 1, fontSize: 16, color: 'green' }

Transforms

Transforms 类似于 CSS 中的变形。可以帮助我们使用 2D 或者 3D 变换来修改组件的外观和位置。

但是需要注意的是,一旦应用了变换,变换后的组件周围的布局将保持不变,因此它可能会与附近的组件重叠。

import React from "react";
import { SafeAreaView, ScrollView, StyleSheet, Text, View } from "react-native";

const App = () => (
  <SafeAreaView style={styles.container}>
    <ScrollView
      contentContainerStyle={styles.scrollContentContainer}
    >
      <View style={styles.box}>
        <Text style={styles.text}>Original Object</Text>
      </View>

      <View style={[styles.box, {
        transform: [{ scale: 2 }]
      }]}>
        <Text style={styles.text}>Scale by 2</Text>
      </View>

      <View style={[styles.box, {
        transform: [{ scaleX: 2 }]
      }]}>
        <Text style={styles.text}>ScaleX by 2</Text>
      </View>

      <View style={[styles.box, {
        transform: [{ scaleY: 2 }]
      }]}>
        <Text style={styles.text}>ScaleY by 2</Text>
      </View>

      <View style={[styles.box, {
        transform: [{ rotate: "45deg" }]
      }]}>
        <Text style={styles.text}>Rotate by 45 deg</Text>
      </View>

      <View style={[styles.box, {
        transform: [
          { rotateX: "45deg" },
          { rotateZ: "45deg" }
        ]
      }]}>
        <Text style={styles.text}>Rotate X&Z by 45 deg</Text>
      </View>

      <View style={[styles.box, {
        transform: [
          { rotateY: "45deg" },
          { rotateZ: "45deg" }
        ]
      }]}>
        <Text style={styles.text}>Rotate Y&Z by 45 deg</Text>
      </View>

      <View style={[styles.box, {
        transform: [{ skewX: "45deg" }]
      }]}>
        <Text style={styles.text}>SkewX by 45 deg</Text>
      </View>

      <View style={[styles.box, {
        transform: [{ skewY: "45deg" }]
      }]}>
        <Text style={styles.text}>SkewY by 45 deg</Text>
      </View>

      <View style={[styles.box, {
        transform: [
          { skewX: "30deg" },
          { skewY: "30deg" }
        ]
      }]}>
        <Text style={styles.text}>Skew X&Y by 30 deg</Text>
      </View>

      <View style={[styles.box, {
        transform: [{ translateX: -50 }]
      }]}>
        <Text style={styles.text}>TranslateX by -50 </Text>
      </View>

      <View style={[styles.box, {
        transform: [{ translateY: 50 }]
      }]}>
        <Text style={styles.text}>TranslateY by 50 </Text>
      </View>
    </ScrollView>
  </SafeAreaView>
);

const styles = StyleSheet.create({
  container: {
    flex: 1
  },
  scrollContentContainer: {
    alignItems: "center",
    paddingBottom: 60
  },
  box: {
    height: 100,
    width: 100,
    borderRadius: 5,
    marginVertical: 40,
    backgroundColor: "#61dafb",
    alignItems: "center",
    justifyContent: "center"
  },
  text: {
    fontSize: 14,
    fontWeight: "bold",
    margin: 8,
    color: "#000",
    textAlign: "center"
  }
});

export default App;

Keyboard

Keyboard 模块用来控制键盘相关的事件。

利用 Keyboard 模块,可以监听原生键盘事件以做出相应回应,比如收回键盘。

import React, { useState, useEffect } from "react";
import { Keyboard, Text, TextInput, StyleSheet, View } from "react-native";

const Example = () => {
  const [keyboardStatus, setKeyboardStatus] = useState(undefined);

  useEffect(() => {
    const showSubscription = Keyboard.addListener("keyboardDidShow", () => {
      setKeyboardStatus("Keyboard Shown");
    });
    const hideSubscription = Keyboard.addListener("keyboardDidHide", () => {
      setKeyboardStatus("Keyboard Hidden");
    });

    return () => {
      showSubscription.remove();
      hideSubscription.remove();
    };
  }, []);

  return (
    <View style={style.container}>
      <TextInput
        style={style.input}
        placeholder='Click here…'
        onSubmitEditing={Keyboard.dismiss}
      />
      <Text style={style.status}>{keyboardStatus}</Text>
    </View>
  );
}

const style = StyleSheet.create({
  container: {
    flex: 1,
    padding: 36
  },
  input: {
    padding: 10,
    borderWidth: 0.5,
    borderRadius: 4
  },
  status: {
    padding: 10,
    textAlign: "center"
  }
});

export default Example;

AppState

RN 开发中,经常会遇到前后台切换的场景。为了监控应用的运行状态,RN 提供了 AppState。通过 AppState 开发者可以很容易地获取应用当前的状态。

AppState 中,应用的状态被分为:

  • active:应用正在前台运行
  • background:应用正在后台运行。用户可能面对以下几种情况:
    • 在别的应用中
    • 停留在桌面
    • 对 Android 来说还可能处在另一个Activity中(即便是由你的应用拉起的)
  • [iOS] inactive:此状态表示应用正在前后台的切换过程中,或是处在系统的多任务视图,又或是处在来电状态中。

要获取当前的状态,你可以使用 AppState.currentState,这个变量会一直保持更新。不过在启动的过程中,currentState 可能为 null,直到 AppState 从原生代码得到通知为止。

import React, { useRef, useState, useEffect } from "react";
import { AppState, StyleSheet, Text, View } from "react-native";

const AppStateExample = () => {
  const appState = useRef(AppState.currentState);
  const [appStateVisible, setAppStateVisible] = useState(appState.current);

  useEffect(() => {
    const subscription = AppState.addEventListener("change", nextAppState => {
      if (
        appState.current.match(/inactive|background/) &&
        nextAppState === "active"
      ) {
        console.log("App has come to the foreground!");
      }

      appState.current = nextAppState;
      setAppStateVisible(appState.current);
      console.log("AppState", appState.current);
    });

    return () => {
      subscription.remove();
    };
  }, []);

  return (
    <View style={styles.container}>
      <Text>Current state is: {appStateVisible}</Text>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
  },
});

export default AppStateExample;

-EOF-

2. 屏幕 API

这一小节我们来看一下 RN 中和屏幕信息相关的 API,主要包括:

  • Dimensions
  • PixelRatio

Dimensions

API 主要用于获取设备屏幕的宽高,Dimensions 的使用比较简单,只需要使用 get 方法即可获取宽高信息,如下所示:

const windowWidth = Dimensions.get('window').width;
const windowHeight = Dimensions.get('window').height;
import React, {Component} from 'react';
import {View, StyleSheet, Text, Dimensions} from 'react-native';

const {width} = Dimensions.get('window');
const {height} = Dimensions.get('window');
const {scale} = Dimensions.get('window');

export default class NetInfoPage extends Component {

    render() {
        return (
            <View style={styles.container}>
                <Text style={styles.textStyle}>屏幕宽:{width}</Text>
                <Text style={styles.textStyle}>屏幕高:{height}</Text>
                <Text style={styles.textStyle}>Scale:{scale}</Text>
            </View>
        );
    }
}

const styles = StyleSheet.create({
    container: {
        flex:1,
        alignItems: 'center',
        justifyContent: 'center',
        backgroundColor: '#F5FCFF'
    },
    btnContainer: {
        marginTop: 100,
        marginLeft: 10,
        marginRight: 10,
        backgroundColor: '#EE7942',
        height: 38,
        borderRadius: 5,
        justifyContent: 'center',
        alignItems: 'center',
    },
    textStyle: {
        fontSize: 28
    }
});

尽管尺寸立即可用,但它们可能会发生变化(例如,由于设备旋转、可折叠设备等),因此任何依赖于这些常量的渲染逻辑或样式都应尝试在每次渲染时调用此函数,而不是缓存值(例如,使用内联样式而不是在样式表中设置值)。

PixelRatio

PixelRatio 可以获取到设备的物理像素和 CSS 像素的比例,也就是 DPR

如果 CSS 像素和设备像素 1:1 关系,那么 DPR 值就为 1。如果 1CSS 像素对应 2 个设备像素,那么 DPR 值就为 2

说简单点,就是一个 CSS 像素要用多少个设备像素来显示。如果 DPR 值为 1,表示用一个设备像素就够了,如果 DPR 值为 2,则表示一个 CSS 像素要用 2 个设备像素来表示。

iPhone4 为例,设备的物理像素为 640,为 CSS 像素为 320,因此 PixelRatio 值为 2

RN 中,通过 PixelRatio.get( ) 方法即可获取 DPR 值。

import React, { Component } from "react";
import { View, StyleSheet, Text, PixelRatio } from "react-native";

const dpr = PixelRatio.get();

export default class App extends Component {
  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.textStyle}>DPR:{dpr}</Text>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: "center",
    justifyContent: "center",
    backgroundColor: "#F5FCFF",
  },
  textStyle: {
    fontSize: 28,
  },
});

常见的屏幕像素密度表如下:

设备像素密度设备
1iPhone2G/3G/3GS 以及 mdpi Android 设备
1.5hdpi Android 设备
2iPhone4/5s/5/5c/5s/6/7/8 以及 xhdpi Android 设备
3iPhone6Plus/6sPlus/7Plus/X/XS/Max 以及 xxhdpi Android 设备
3.5Nexus6/PixelXL/2XL Android 设备

我们通过前面的学习已经知道,在 RN 中所有尺寸都是没有单位的,例如:width: 100,这是因为 RN 中尺寸只有一个单位 dp,这是一种基于屏幕密度的抽象单位,默认省略。

在 RN 中,我们可以通过 PixelRatio 来将真实像素大小和 dp 单位进行一个转换

  • static getPixelSizeForLayoutSize(layoutSize: number): number:获取一个布局元素的真实像素大小,返回值是一个四舍五入的整型
  • static roundToNearestPixel(px: number): number:将真实像素大小转为 RN 的 dp 单位
import { PixelRatio } from 'react-native';
const dp2px = dp => PixelRatio.getPixelSizeForLayoutSize(dp);
const px2dp = px => PixelRatio.roundToNearestPixel(px);

//按照下面的方式可实现px与dp之间的转换(比如100px*200px的View)
<View style={{width:px2dp(100),height:px2dp(200),backgroundColor:"red"}}/>

-EOF-

3. 设备 API

设备 API 主要用于获取当前用户的设备相关信息,从而根据不同的设备信息来做出可能不同的操作。主要包括:

  • Platform
  • PlatformColor
  • Appearance

Platform

Platform 主要用于获取设备的相关信息。下面是官方提供的一个示例:

import React from 'react';
import { Platform, StyleSheet, Text, ScrollView } from 'react-native';

const App = () => {
  return (
    <ScrollView contentContainerStyle={styles.container}>
      <Text>OS</Text>
      <Text style={styles.value}>{Platform.OS}</Text>
      <Text>OS Version</Text>
      <Text style={styles.value}>{Platform.Version}</Text>
      <Text>isTV</Text>
      <Text style={styles.value}>{Platform.isTV.toString()}</Text>
      {Platform.OS === 'ios' && <>
        <Text>isPad</Text>
        <Text style={styles.value}>{Platform.isPad.toString()}</Text>
      </>}
      <Text>Constants</Text>
      <Text style={styles.value}>
        {JSON.stringify(Platform.constants, null, 2)}
      </Text>
    </ScrollView>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  value: {
    fontWeight: '600',
    padding: 4,
    marginBottom: 8
  }
});

export default App;

PlatformColor

每个平台都有系统定义的颜色,尽管可以通过 AppearanceAPIAccessibilityInfo 检测并设置其中的某些样式,但是这样的操作不仅开发成本高昂,而且还局限。

RN 从 0.63 版本开始提供了一个开箱即用的解决方案来使用这些系统颜色。PlatformColor 是一个新的 API,可以像 RN 中的其它任何颜色一样使用。

例如,在 iOS 上,系统提供一种颜色 labelColor,可以在 RN 中这样使用 PlatformColor

import { Text, PlatformColor } from 'react-native';

<Text style={{ color: PlatformColor('labelColor') }}>
  This is a label
</Text>;

另一方面,Android 提供像 colorButtonNormal 这样的颜色,可以在 RN 中这样使用 PlatformColor

import { View, Text, PlatformColor } from 'react-native';

<View
  style={{
    backgroundColor: PlatformColor('?attr/colorButtonNormal')
  }}>
  <Text>This is colored like a button!</Text>
</View>;

同时 DynamicColorIOS 是仅限于 iOSAPI,可以定义在浅色和深色模式下使用的颜色。与 PlatformColor 相似,可以在任何可以使用颜色的地方使用:

import { Text, DynamicColorIOS } from 'react-native';

const customDynamicTextColor = DynamicColorIOS({
  dark: 'lightskyblue',
  light: 'midnightblue'
});

<Text style={{ color: customDynamicTextColor }}>
  This color changes automatically based on the system theme!
</Text>;

下面是来自官方的一个示例:

import React from 'react';
import {
  Platform,
  PlatformColor,
  StyleSheet,
  Text,
  View
} from 'react-native';

const App = () => (
  <View style={styles.container}>
    <Text style={styles.label}>
      I am a special label color!
    </Text>
  </View>
);

const styles = StyleSheet.create({
  label: {
    padding: 16,
    ...Platform.select({
      ios: {
        color: PlatformColor('label'),
        backgroundColor:
          PlatformColor('systemTealColor'),
      },
      android: {
        color: PlatformColor('?android:attr/textColor'),
        backgroundColor:
          PlatformColor('@android:color/holo_blue_bright'),
      },
      default: { color: 'black' }
    })
  },
  container: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
  }
});

export default App;

Appearance

Appearance 模块主要用于获取用户当前的外观偏好。目前的手机系统一般都可以选择浅色模式和深色模式,通过 Appearance 模块,开发者就可以获取此信息。

Appearance 模块提供了一个 getColorScheme 的静态方法,该方法可以获取当前用户首选的配色方案,对应的值有 3 个:

  • light: 浅色主题
  • dark: 深色主题
  • null: 没有选择外观偏好

例如:

import React from "react";
import {
  StyleSheet,
  Text,
  View,
  Appearance,
} from "react-native";

const App = () => {
  return (
    <View style={styles.container}>
      <Text>外观偏好</Text>
      <Text style={styles.value}>{Appearance.getColorScheme()}</Text>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
  },
  value: {
    fontWeight: "600",
    padding: 4,
    marginBottom: 8,
  },
});

export default App;

-EOF-

4. 动画 API

这一小节我们来看一下 RN 中和动画相关的 API,主要包括:

  • LayoutAnimation
  • Animated

LayoutAnimation

LayoutAnimationRN 提供的一套全局布局动画 API,只需要配置好动画的相关属性(例如大小、位置、透明度),然后调用组件的状态更新方法引起重绘,这些布局变化就会在下一次渲染时以动画的形式呈现。

Andriod 设备上使用 LayoutAnimation,需要通过 UIManager 手动启用,并且需要放在任何动画代码之前,比如可以放在入口文件 App.js 中。

if (Platform.OS === 'android') {
  if (UIManager.setLayoutAnimationEnabledExperimental) {
    UIManager.setLayoutAnimationEnabledExperimental(true);
  }
}

下面我们来看一个示例:

const customAnim = {
  customSpring: {
    duration: 400,
    create: {
      type: LayoutAnimation.Types.spring,
      property: LayoutAnimation.Properties.scaleXY,
      springDamping: 0.6,
    },
    update: {
      type: LayoutAnimation.Types.spring,
      springDamping: 0.6,
    },
  },
  customLinear: {
    duration: 200,
    create: {
      type: LayoutAnimation.Types.linear,
      property: LayoutAnimation.Properties.opacity,
    },
    update: {
      type: LayoutAnimation.Types.easeInEaseOut,
    },
  },
};

在上面的代码中,我们定义了 customAnim 是一个对象,该对象包含了两种动画方式,一种是 customSpring,另一种是 customLinear

每一种动画都用对象来描述,包含 4 个可选值:

  • duration:动画的时长
  • create:组件创建时的动画
  • update:组件更新时的动画
  • delete:组件销毁时的动画

customSpring 为例,对应的 duration400 毫秒,而 createupdate 包括 delete 对应的又是一个对象,其类型定义如下:

type Anim = {
    duration? : number, // 动画时常
    delay? : number, // 动画延迟
    springDamping? : number, // 弹跳动画阻尼系数
    initialV elocity? : number, // 初始速度
    type? : $Enum<typeof TypesEnum> // 动画类型
    property? : $Enum<typeof PropertiesEnum> // 动画属性
}

其中 type 定义在 LayoutAnimation.Types 中,常见的动画类型有:

  • spring:弹跳动画
  • linear:线性动画
  • easeInEaseOut:缓入缓出动画
  • easeIn:缓入动画
  • easeOut:缓出动画

动画属性 property 定义在 LayoutAnimation.Properties 中,支持的动画属性有:

  • opacity:透明度
  • scaleXY:缩放

因此,上面我们所定义的 customSpring 动画的不同属性值也就非常清晰了。

customSpring: {
    duration: 400,
    create: {
      type: LayoutAnimation.Types.spring,
      property: LayoutAnimation.Properties.scaleXY,
      springDamping: 0.6,
    },
    update: {
      type: LayoutAnimation.Types.spring,
      springDamping: 0.6,
    },
},

下面附上该示例的完整代码:

import React, { useState } from "react";
import {
  View,
  StyleSheet,
  Text,
  LayoutAnimation,
  TouchableOpacity,
  UIManager,
} from "react-native";

if (
  Platform.OS === "android" &&
  UIManager.setLayoutAnimationEnabledExperimental
) {
  UIManager.setLayoutAnimationEnabledExperimental(true);
}

const customAnim = {
  customSpring: {
    duration: 400,
    create: {
      type: LayoutAnimation.Types.spring,
      property: LayoutAnimation.Properties.scaleXY,
      springDamping: 0.6,
    },
    update: {
      type: LayoutAnimation.Types.spring,
      springDamping: 0.6,
    },
  },
  customLinear: {
    duration: 200,
    create: {
      type: LayoutAnimation.Types.linear,
      property: LayoutAnimation.Properties.opacity,
    },
    update: {
      type: LayoutAnimation.Types.easeInEaseOut,
    },
  },
};

const App = () => {
  const [width, setWidth] = useState(200);
  const [height, setHeight] = useState(200);
  const [whichAni,setWhichAni] = useState(true);

  function largePress() {
    whichAni ? 
    LayoutAnimation.configureNext(customAnim.customSpring) :
    LayoutAnimation.configureNext(customAnim.customLinear);
    setWhichAni(!whichAni);
    setWidth(width + 20);
    setHeight(height + 20);
  }

  return (
    <View style={styles.container}>
      <View style={[styles.content, { width, height }]} />
      <TouchableOpacity style={styles.btnContainer} onPress={largePress}>
        <Text style={styles.textStyle}>点击增大</Text>
      </TouchableOpacity>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: "center",
    justifyContent: "center",
    backgroundColor: "#F5FCFF",
  },
  content: {
    backgroundColor: "#FF0000",
    marginBottom: 10,
  },
  btnContainer: {
    marginTop: 30,
    marginLeft: 10,
    marginRight: 10,
    backgroundColor: "#EE7942",
    height: 38,
    width: 320,
    borderRadius: 5,
    justifyContent: "center",
    alignItems: "center",
  },
  textStyle: {
    fontSize: 18,
    color: "#ffffff",
  },
});

export default App;

当然,如果不想那么麻烦的进行配置,LayoutAnimation 也提供了一些 linear、spring 的替代方法,这些替代方法会直接使用默认值。

例如:

function largePress() {
    whichAni ? 
    LayoutAnimation.spring() :
    LayoutAnimation.linear();
    setWhichAni(!whichAni);
    setWidth(width + 20);
    setHeight(height + 20);
}

Animated

前面所学习的 LayoutAnimation 称为布局动画,这种方法使用起来非常便捷,它会在如透明度渐变、缩放这类变化时触发动画效果,动画会在下一次渲染或布局周期运行。布局动画还有个优点就是无需使用动画化组件,如 Animated.View

AnimatedRN 提供的另一种动画方式,相较于 LayoutAnimation,它更为精细,可以只作为单个组件的单个属性,也可以更加手势的响应来设定动画(例如通过手势放大图片等行为),甚至可以将多个动画变化组合到一起,并可以根据条件中断或者修改。

下面我们先来看一个快速入门示例:

import React, { useState } from "react";
import { Animated, Text, View, StyleSheet, Button, Easing } from "react-native";

const App = () => {
  // fadeAnim will be used as the value for opacity. Initial Value: 0
  const [fadeInValue, setFadeInValue] = useState(new Animated.Value(0));

  const fadeIn = () => {
    // Will change fadeAnim value to 1 in 5 seconds
    Animated.timing(fadeInValue, {
      toValue: 1,
      duration: 2000,
      easing: Easing.linear,
      useNativeDriver: true,
    }).start();
  };

  const fadeOut = () => {
    // Will change fadeAnim value to 0 in 3 seconds
    Animated.timing(fadeInValue, {
      toValue: 0,
      duration: 3000,
      useNativeDriver: true,
    }).start();
  };

  return (
    <View style={styles.container}>
      <Animated.View
        style={[
          styles.fadingContainer,
          {
            // Bind opacity to animated value
            opacity: fadeInValue,
          },
        ]}
      >
        <Text style={styles.fadingText}>Fading View!</Text>
      </Animated.View>
      <View style={styles.buttonRow}>
        <Button title="Fade In View" onPress={fadeIn} />
        <Button title="Fade Out View" onPress={fadeOut} />
      </View>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: "center",
    justifyContent: "center",
  },
  fadingContainer: {
    padding: 20,
    backgroundColor: "powderblue",
  },
  fadingText: {
    fontSize: 28,
  },
  buttonRow: {
    flexBasis: 100,
    justifyContent: "space-evenly",
    marginVertical: 16,
  },
});

export default App;

在上面的代码中,我们书写了一个淡入淡出的效果。下面我们来分析其中关键的代码。

const [fadeInValue, setFadeInValue] = useState(new Animated.Value(0));

App 组件中,我们定义了一个状态 fadeInValue,该状态的初始值为 new Animated.Value(0),这就是设置动画的初始值。

<Animated.View
    style={[
      styles.fadingContainer,
      {
        // Bind opacity to animated value
        opacity: fadeInValue,
      },
    ]}
>
    <Text style={styles.fadingText}>Fading View!</Text>
</Animated.View>

接下来,我们将要应用动画的组件包裹在 Animated.View 组件中,然后将 Animated.Value 绑定到组件的 style 属性上。

之后点击按钮的时候,我们要控制 Text 的显隐效果,按钮各自绑定事件,对应的代码:

const fadeIn = () => {
    // Will change fadeAnim value to 1 in 5 seconds
    Animated.timing(fadeInValue, {
      toValue: 1,
      duration: 2000,
      easing: Easing.linear,
      useNativeDriver: true,
    }).start();
};

const fadeOut = () => {
    // Will change fadeAnim value to 0 in 3 seconds
    Animated.timing(fadeInValue, {
      toValue: 0,
      duration: 3000,
      useNativeDriver: true,
    }).start();
};

在事件处理函数中,使用 Animated.timing 方法并设置动画参数,最后调用 start 方法启动动画。

timing 对应的参数属性如下:

  • duration: 动画的持续时间,默认为 500
  • easing: 缓动动画,默认为 Easing.inOut
  • delay: 开始动画前的延迟时间,默认为 0
  • isInteraction: 指定本动画是否在 InteractionManager 的队列中注册以影响任务调度,默认值为 true
  • useNativeDriver: 是否启用原生动画驱动,默认为 false

除了 timing 动画,Animated 还支持 decayspring。每种动画类型都提供了特定的函数曲线,用于控制动画值从初始值到最终值的变化过程。

  • decay:衰减动画,以一个初始速度开始并且逐渐减慢停止
  • spring:弹跳动画,基于阻尼谐振动的弹性动画
  • timing:渐变动画,按照线性函数执行的动画

Animated 动画 API 中,decay、springtiming 是动画的核心,其他复杂动画都可以使用这三种动画类型来实现。

除了上面介绍的动画 API 之外,Animated 还支持复杂的组合动画,如常见的串行动画和并行动画。Animated 可以通过以下的方法将多个动画组合起来。

  • parallel:并行执行
  • sequence:顺序执行
  • stagger:错峰执行,其实就是插入 delayparallel 动画

来看一个示例:

import React, { Component } from "react";
import {
  View,
  StyleSheet,
  Text,
  Animated,
  Easing,
  TouchableOpacity,
} from "react-native";

/**
 * 串行动画
 */
export default class AnimatedTiming extends Component {
  constructor(props) {
    super(props);
    this.state = {
      bounceValue: new Animated.Value(0),
      rotateValue: new Animated.Value(0),
    };
  }

  onPress() {
    Animated.sequence([
      //串行动画函数
      Animated.spring(this.state.bounceValue, { toValue: 1,useNativeDriver: true }), //弹性动画
      Animated.delay(500),
      Animated.timing(this.state.rotateValue, {
        //渐变动画
        toValue: 1,
        duration: 800,
        easing: Easing.out(Easing.quad),
        useNativeDriver: true
      }),
    ]).start(() => this.onPress()); //开始执行动画
  }

  render() {
    return (
      <View style={styles.container}>
        <Animated.View
          style={[
            styles.content,
            {
              transform: [
                {
                  rotate: this.state.rotateValue.interpolate({
                    inputRange: [0, 1],
                    outputRange: ["0deg", "360deg"],
                  }),
                },
                {
                  scale: this.state.bounceValue,
                },
              ],
            },
          ]}
        >
          <Text style={styles.content}>Hello World!</Text>
        </Animated.View>
        <TouchableOpacity
          style={styles.btnContainer}
          onPress={this.onPress.bind(this)}
        >
          <Text style={styles.textStyle}>串行动画</Text>
        </TouchableOpacity>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: "center",
    justifyContent: "center",
    backgroundColor: "#F5FCFF",
  },
  content: {
    backgroundColor: "#FF0000",
    marginBottom: 10,
    padding: 10,
  },
  btnContainer: {
    marginTop: 30,
    marginLeft: 10,
    marginRight: 10,
    backgroundColor: "#EE7942",
    height: 38,
    width: 320,
    borderRadius: 5,
    justifyContent: "center",
    alignItems: "center",
  },
  textStyle: {
    fontSize: 18,
    color: "#ffffff",
  },
});

在上面我们就是使用的 Animated.sequence 顺序执行,如果想要并行执行,可以将上面 Animated.sequence 部分代码修改为:

Animated.parallel([
  //串行动画函数
  Animated.spring(this.state.bounceValue, { toValue: 1,useNativeDriver: true }), //弹性动画
  Animated.timing(this.state.rotateValue, {
    //渐变动画
    toValue: 1,
    duration: 800,
    easing: Easing.out(Easing.quad),
    useNativeDriver: true
  }),
]).start(() => this.onPress()); //开始执行动画

关于动画化组件,前面我们使用的是 Animated.View,目前官方提供的动画化组件有 6 种:

  • Animated.Image
  • Animated.ScrollView
  • Animated.Text
  • Animated.View
  • Animated.FlatList
  • Animated.SectionList

它们非常强大,基本可以满足大部分动画需求,在实际应用场景中,可以应用于透明度渐变、位移、缩放、颜色的变化等。

除了上面介绍的一些常见的动画场景,Animated 还支持手势控制动画。手势控制动画使用的是 Animated.event,它支持将手势或其他事件直接绑定到动态值上。

来看一个示例,下面是使用 Animated.event 实现图片水平滚动时的图片背景渐变效果。

import React, { useState } from "react";
import {
  ScrollView,
  Animated,
  Image,
  View,
  StyleSheet,
  Dimensions,
} from "react-native";

const { width } = Dimensions.get("window");

const App = () => {
  const [xOffset, setXOffset] = useState(new Animated.Value(1.0));

  return (
    <View style={styles.container}>
      <ScrollView
        horizontal={true}
        showsHorizontalScrollIndicator={false}
        style={styles.imageStyle}
        onScroll={Animated.event(
          [{ nativeEvent: { contentOffset: { x: xOffset } } }],
          { useNativeDriver: false }
        )}
        scrollEventThrottle={100}
      >
        <Animated.Image
          source={{ uri: "http://doc.zwwill.com/yanxuan/imgs/banner-1.jpg" }}
          style={[
            styles.imageStyle,
            {
              opacity: xOffset.interpolate({
                inputRange: [0, 375],
                outputRange: [1.0, 0.0],
              }),
            },
          ]}
          resizeMode="cover"
        />
        <Image
          source={{ uri: "http://doc.zwwill.com/yanxuan/imgs/banner-2.jpg" }}
          style={styles.imageStyle}
          resizeMode="cover"
        />
      </ScrollView>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: "center",
    marginTop: 44,
    justifyContent: "center",
    backgroundColor: "#F5FCFF",
  },
  imageStyle: {
    height: 200,
    width: width,
  },
});

export default App;

ScrollView 逐渐向左滑动时,左边的图片的透明度会逐渐降为 0

作为提升用户体验的重要手段,动画对于移动应用程序来说是非常重要的,因此合理地使用动画是必须掌握的一项技能。


-EOF-

6. 手势 API

这一小节我们来看一下 RN 中和手势相关的 API

文档地址:reactnative.cn/docs/panres…

我们先来看一个简单的例子:

import React from "react";
import { PanResponder, StyleSheet, View } from "react-native";

export default function App() {
  const panResponder = PanResponder.create({
    onStartShouldSetPanResponder: function () {
      console.log("shouldstart");
      return true;
    },
    onPanResponderMove: function () {
      console.log("moving");
    },
    onPanResponderRelease: function () {
      console.log("release");
    },
  });

  console.log(panResponder.panHandlers);

  return (
    <View style={styles.container}>
      <View style={styles.box} {...panResponder.panHandlers}></View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
  },
  box: {
    backgroundColor: "#61dafb",
    width: 80,
    height: 80,
    borderRadius: 4,
  },
});

在上面的示例中,我们通过 PanResponder 这个 APIcreate 方法来创建一个手势方法的集合对象。该方法接收一个配置对象,配置对象中能够传递的参数如下:

image-20220623150525365

可以看到,配置对象对应的每一个配置值都是一个回调函数,每个回调函数都接收两个参数,一个是原生事件对象,另一个是 gestureState 对象。

nativeEvent 原生事件对象有如下字段:

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

一个 gestureState 对象有如下的字段:

  • stateID - 触摸状态的 ID。在屏幕上有至少一个触摸点的情况下,这个 ID 会一直有效。
  • moveX - 最近一次移动时的屏幕横坐标
  • moveY - 最近一次移动时的屏幕纵坐标
  • x0 - 当响应器产生时的屏幕坐标
  • y0 - 当响应器产生时的屏幕坐标
  • dx - 从触摸操作开始时的累计横向路程
  • dy - 从触摸操作开始时的累计纵向路程
  • vx - 当前的横向移动速度
  • vy - 当前的纵向移动速度
  • numberActiveTouches - 当前在屏幕上的有效触摸点的数量

例如我们通过 gestureState 对象来判断用户手指的移动方向:

const panResponder = PanResponder.create({
    onStartShouldSetPanResponder: function () {
      console.log("shouldstart");
      return true;
    },
    onPanResponderMove: function (e, gs) {
      console.log(`正在移动: X轴: ${gs.dx}, Y轴: ${gs.dy}`);
    },
    onPanResponderRelease: function (e, gs) {
      console.log(`结束移动: X轴移动了: ${gs.dx}, Y轴移动了: ${gs.dy}`);
      if (gs.dx > 50) {
        console.log("由左向右");
      } else if (gs.dx < -50) {
        console.log("由右向左");
      } else if (gs.dy > 50) {
        console.log("由上向下");
      } else if (gs.dy < -50) {
        console.log("由下向上");
      }
    },
});

最后,我们把上一节课介绍的 Animated 结合起来,书写一个拖动小方块的示例:

import { useState } from "react";
import { Animated, PanResponder, StyleSheet, View } from "react-native";

export default function App() {
  const [transXY] = useState(new Animated.ValueXY());

  const panResponder = PanResponder.create({
    onStartShouldSetPanResponder: function () {
      console.log("shouldstart");
      return true;
    },
    onPanResponderMove: Animated.event(
      [
        null,
        {
          dx: transXY.x,
          dy: transXY.y,
        },
      ],
      { useNativeDriver: false }
    ),

    onPanResponderRelease: function () {
      Animated.spring(transXY, {
        toValue: { x: 0, y: 0 },
        useNativeDriver: false,
      }).start();
    },
  });

  return (
    <View style={styles.container}>
      <Animated.View
        style={[
          styles.box,
          {
            transform: [{ translateX: transXY.x }, { translateY: transXY.y }],
          },
        ]}
        {...panResponder.panHandlers}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
  },
  box: {
    backgroundColor: "#61dafb",
    width: 80,
    height: 80,
    borderRadius: 4,
  },
});

-EOF-