本文的范围将涵盖对 Three.js 库和 Animated API 的探索。 您应该具备 JavaScript 和 React Native 的基本知识才能继续学习; 要了解更多关于可以在 React Native 中完成的所有美妙事情, 的React Native 档案是一个很好的学习场所。 LogRocket 博客上
我们将这篇文章一分为二。 在第一部分,我们探索在 React Native 中创建高级 3D 动画。 我们将依赖 Three.js 库,它是一个跨平台 JavaScript 3D 库,用于在 Web 环境中创建 3D 内容,例如图形和动画。 Three.js 结合了 WebGL 增强行为以在 Web 上渲染 3D 模型和 TweenMax 以增强动画质量。
本文的第二部分将探索 Animated API ,它使我们的动画变得流畅。
跳跃前进:
安装和先决条件
如何使用 Three.js 渲染 3D 模型
使用 3D 立方体创建场景
探索动画 API
使用 Animated API 创建 3D 效果
添加背景
首先,我们需要创建我们的 React Native 应用程序。 安装 Expo CLI 为我们的项目服务; 它与 Expo GO 库协同工作,这是一个移动客户端应用程序,我们将使用它在 iOS 或 Android 平台上打开我们的项目。
安装 Expo CLI 后,继续在终端中运行以下命令。 此应用程序将使用 TypeScript 模板。
expo init reactNative3D cd reactNative3D yarn start
在继续之前,我们需要安装一些核心依赖项。 打开终端窗口,运行以下命令。
yarn add three expo-three expo-gl yarn add --dev @types/three
让我们回顾一下这些依赖项:
expo-gl :这提供了一个 View充当 OpenGL ES 渲染目标,这对于渲染 2D 和 3D 图形非常有用。 挂载后,会创建一个 OpenGL 上下文,该上下文接受 onContextCreate prop,它有一个 WebGL RenderingContext 接口
expo-three : 作为 Three.js 和 ExpoGL 之间的桥梁; 它还为 React Native 中的原生 OpenGL-ES 提供了一个 WebGL 接口,这有助于将 DOM 从 Three.js 中抽象出来
三 :用于在网页上创建 3D 内容的 3D 库
当使用 Three.js 渲染 3D 模型时,我们首先创建一个场景作为模型渲染的集合。下图说明了 Three.js 应用程序的基本结构,其中需要创建对象并将它们连接起来一起。
来源: Three.js
让我们探索一下上面的图表。
超过 20 万开发人员使用 LogRocket 来创造更好的数字体验 了解更多 →
关键部分是 renderer, Three.js 的主要对象。 我们创建的 scene和 camera被传递给渲染器,渲染器渲染(绘制)3D场景的一部分
这 scene是一个定义根的对象 scenegraph并包含一些属性,例如背景颜色
Mesh是表示特定绘图的对象 Geometry具有特定的 Material班级
一块的顶点数据 Geometry(球体,立方体)由 Geometry目的。 Three.js 提供了内置的几何原语
用于绘制几何图形的表面属性由 Material目的。 它接受诸如 color和 texture
Texture对象表示从图像文件加载的图像
在以下部分中,我们将使用这些结构中的每一个来创建 3D 动画。
在里面 App.tsx在我们项目目录的根目录下,我们将创建一个基本的 React Native 组件。 将需要的包导入到 App.tsx零件。
code App.tsx import React from 'react'; import { View } from 'react-native'; import Expo from 'expo'; import {Scene, Mesh, MeshBasicMaterial, PerspectiveCamera} from 'three'; import ExpoTHREE, {Renderer} from 'expo-three'; import { ExpoWebGLRenderingContext, GLView } from 'expo-gl';
继续创建场景, GLView exported from expo-gl provides a view that acts as an OpenGL ES render target. This is very useful for rendering the 3D objects we’re creating.
在里面 App.tsx组件,创建一个功能组件。
const App = () => { const onContextCreate = async (gl: Object) => {} return ( <View> <GLView onContextCreate={onContextCreate} /> </View> ) } export default App;
我们的应用程序的基本骨架已经完成。 这 onContextCreate道具被传递到 GLView用一个论点, gl,它有一个 WebGL RenderingContext 接口。
转移我们的注意力,让我们创造 onContextCreate功能。
const onContextCreate = async (gl: any) => { // three.js implementation. const scene = new Scene(); const camera = new PerspectiveCamera( 75, gl.drawingBufferWidth / gl.drawingBufferHeight, 0.1, 1000 ); gl.canvas = { width: gl.drawingBufferWidth, height: gl.drawingBufferHeight, }; // set camera position away from cube camera.position.z = 2; const renderer = new Renderer({ gl }); // set size of buffer to be equal to drawing buffer width renderer.setSize(gl.drawingBufferWidth, gl.drawingBufferHeight); // create cube // define geometry const geometry = new BoxBufferGeometry(1, 1, 1); const material = new MeshBasicMaterial({ color: "cyan", }); const cube = new Mesh(geometry, material); // add cube to scene scene.add(cube); // create render function const render = () => { requestAnimationFrame(render); // create rotate functionality // rotate around x axis cube.rotation.x += 0.01; // rotate around y axis cube.rotation.y += 0.01; renderer.render(scene, camera); gl.endFrameEXP(); }; // call render render(); };
With the completion of the onContextCreate function, our 3D cube is complete.
Your App.tsx file should look like this:
import React from "react"; import { View } from "react-native"; import Expo from "expo"; import { Scene, Mesh, MeshBasicMaterial, PerspectiveCamera, BoxBufferGeometry, } from "three"; import ExpoTHREE, { Renderer } from "expo-three"; import { ExpoWebGLRenderingContext, GLView } from "expo-gl"; import { StatusBar } from "expo-status-bar"; const App = () => { const onContextCreate = async (gl: any) => { // three.js implementation. const scene = new Scene(); const camera = new PerspectiveCamera( 75, gl.drawingBufferWidth / gl.drawingBufferHeight, 0.1, 1000 ); gl.canvas = { width: gl.drawingBufferWidth, height: gl.drawingBufferHeight, }; // set camera position away from cube camera.position.z = 2; const renderer = new Renderer({ gl }); // set size of buffer to be equal to drawing buffer width renderer.setSize(gl.drawingBufferWidth, gl.drawingBufferHeight); // create cube // define geometry const geometry = new BoxBufferGeometry(1, 1, 1); const material = new MeshBasicMaterial({ color: "cyan", }); const cube = new Mesh(geometry, material); // add cube to scene scene.add(cube); // create render function const render = () => { requestAnimationFrame(render); // create rotate functionality // rotate around x axis cube.rotation.x += 0.01; // rotate around y axis cube.rotation.y += 0.01; renderer.render(scene, camera); gl.endFrameEXP(); }; // call render render(); }; return ( <View> <GLView onContextCreate={onContextCreate} // set height and width of GLView style={{ width: 400, height: 400 }} /> </View> ); }; export default App;
停止 Metro 服务器以确保已添加所有新文件并重新启动它。
不要错过 The Replay 来自 LogRocket 的精选时事通讯
了解 LogRocket 的 Galileo 如何消除噪音以主动解决应用程序中的问题
使用 React 的 useEffect 优化应用程序的性能
之间切换 在多个 Node 版本
了解如何 使用 AnimXYZ 为您的 React 应用程序制作动画
探索 Tauri ,一个用于构建二进制文件的新框架
比较 NestJS 与 Express.js
ctrl c yarn start
使用 Expo 应用程序打开应用程序。
In this section, we’ll create a 3D carousel using a FlatList and the Animated API. Let’s first create the carousel without the 3D effect.
In the App.tsx, comment out the previous code and start the new implementation from scratch. We begin by installing the dependencies we’ll need in the project.
安装 react-native-uuid 库和 @expo/vector-icons 。
yarn add react-native-uuid @expo/vector-icons
现在,将所需的库导入到组件中。
import * as React from "react"; import { FlatList, Image, Text, View, Dimensions, TouchableOpacity, StyleSheet, Animated, } from "react-native"; import { SafeAreaView } from "react-native"; import { AntDesign } from "@expo/vector-icons"; import uuid from "react-native-uuid"; import { StatusBar } from "expo-status-bar"; const { width, height } = Dimensions.get("screen");
创建图像轮播时,指定 width和 height轮播中图像的属性可以提供更好的视图。 这 Spacing变量支持跨不同样式需求的可重用性。
const IMAGE_WIDTH = width * 0.65; const IMAGE_HEIGHT = height * 0.7; const SPACING = 20;
使用 Pexels Images API ,我们可以生成一组图像来填充我们的应用程序。
const images = [ "https://images.pexels.com/photos/1799912/pexels-photo-1799912.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500", "https://images.pexels.com/photos/1769524/pexels-photo-1769524.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500", "https://images.pexels.com/photos/1758101/pexels-photo-1758101.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500", "https://images.pexels.com/photos/1738434/pexels-photo-1738434.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500", "https://images.pexels.com/photos/1698394/pexels-photo-1698394.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500", "https://images.pexels.com/photos/1684429/pexels-photo-1684429.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500", "https://images.pexels.com/photos/1690351/pexels-photo-1690351.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500", "https://images.pexels.com/photos/1668211/pexels-photo-1668211.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500", "https://images.pexels.com/photos/1647372/pexels-photo-1647372.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500", "https://images.pexels.com/photos/1616164/pexels-photo-1616164.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500", "https://images.pexels.com/photos/1799901/pexels-photo-1799901.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500", "https://images.pexels.com/photos/1789968/pexels-photo-1789968.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500", "https://images.pexels.com/photos/1774301/pexels-photo-1774301.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500", "https://images.pexels.com/photos/1734364/pexels-photo-1734364.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500", "https://images.pexels.com/photos/1724888/pexels-photo-1724888.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500", ];
我们将使用 react-native-uuid 库 将随机数据播种到应用程序中。
const DATA = [...Array(images.length).keys()].map((_, i) => { return { key: uuid.v4(), image: images[i], }; });
现在是时候实现我们的轮播视图了。
export default () => { return ( <View style={{ backgroundColor: "#A5F1FA", flex: 1 }}> <StatusBar hidden /> <SafeAreaView style={{ marginTop: SPACING * 1 }}> <View style={{ height: IMAGE_HEIGHT * 2.1 }}> <FlatList data={DATA} keyExtractor={(item) => item.key} horizontal pagingEnabled bounces={false} style={{ flexGrow: 0, zIndex: 9999 }} contentContainerStyle={{ height: IMAGE_HEIGHT + SPACING * 2, paddingHorizontal: SPACING * 4, }} showsHorizontalScrollIndicator={false} renderItem={({ item, index }) => { return ( <View style={{ width, paddingVertical: SPACING, }} > <Image source={{ uri: item.image }} style={{ width: IMAGE_WIDTH, height: IMAGE_HEIGHT, resizeMode: "cover", }} /> </View> ); }} /> </View> </SafeAreaView> </View> ); };
图像轮播已成功创建。
下一步是使用 Animated API 创建 3D 效果。 为了使用动画 API,我们需要改变我们的 FlatList对一个 Animated.FlatList并添加一个 onScroll事件,我们将在其中传递一个 NativeEvent.
一个变量 scrollX将被定义为我们的 x 轴的值。 我们将传入一个 useRef()Hook 使 React 能够跟踪动画。 这坚持了 scrollX即使重新渲染后的价值。
export default () => { const scrollX = React.useRef(new Animated.Value(0)).current; return ( <View style={{ backgroundColor: "#A5F1FA", flex: 1 }}> <StatusBar hidden /> <SafeAreaView style={{ marginTop: SPACING * 1 }}> <View style={{ height: IMAGE_HEIGHT * 2.1 }}> <Animated.FlatList data={DATA} keyExtractor={(item) => item.key} horizontal pagingEnabled onScroll={Animated.event( [ { nativeEvent: { contentOffset: { x: scrollX } }, }, ], { useNativeDriver: true, } )}
现在我们可以在依赖的同时插入值 scrollX创建动画。 在里面 renderItem我们的 FlatList, 创建一个 inputRange. 我们将使用输入范围数字进行插值。 然后,创建一个 opacity变量内 renderItem.
renderItem={({ item, index }) => { const inputRange = [ (index - 1) * width, // next slide index * width, // current slide (index + 1) * width, // previous slide ]; const opacity = scrollX.interpolate({ inputRange, outputRange: [0, 1, 0], }); const translateY = scrollX.interpolate({ inputRange, outputRange: [50, 0, 20] // create a wave })
继续前进,我们已将项目中的视图转换为 Animated.View, 和 opacity我们之前创建的变量将作为样式传入。
return ( <Animated.View style={{ width, paddingVertical: SPACING, opacity, transform: [{ translateY }] }} > <Image source={{ uri: item.image }} style={{ width: IMAGE_WIDTH, height: IMAGE_HEIGHT, resizeMode: "cover", }} /> </Animated.View> );
现在在滑动时,根据输入范围应用不透明度。
让我们在滑动图像时添加一个白色背景以突出 3D 动画。
在下面 View,粘贴下面的代码块。
<View style={{ width: IMAGE_WIDTH + SPACING * 4, height: 450, position: "absolute", backgroundColor: "white", backfaceVisibility: true, zIndex: -1, top: SPACING * 1, left: SPACING * 1.7, bottom: 0, shadowColor: "#000", shadowOpacity: 0.2, shadowRadius: 24, shadowOffset: { width: 0, height: 0, }, }} /> </View>
下一步是为白色背景设置动画,使其在 3D 视图中旋转。 但在我们这样做之前,让我们想办法查看 inputRange之间 0和 1.
在我们的顶部 Carousel组件,使用方法创建一个进度变量 divide()和 modulo()来自 Animated API,它让我们修改并获取之间的值 0和 1. 这 progress变量使我们能够将我们的值限制在 0和 1.
export default () => { const scrollX = React.useRef(new Animated.Value(0)).current; const progress = Animated.modulo(Animated.divide(scrollX, width), width);
我们现在准备开始修改 View持有我们的白色背景的组件。 正如我们之前所做的那样,将 View组件成 Animated.View.
一个 transform输入被传递到 Animated.View零件; 这 transform收到一个 perspective和一个 rotateY.
<Animated.View style={{ width: IMAGE_WIDTH + SPACING * 4, height: 450, position: "absolute", backgroundColor: "white", backfaceVisibility: true, zIndex: -1, top: SPACING * 1, left: SPACING * 1.7, bottom: 0, shadowColor: "#000", shadowOpacity: 0.2, shadowRadius: 24, shadowOffset: { width: 0, height: 0, }, transform: [ { perspective: IMAGE_WIDTH * 4, }, { rotateY: progress.interpolate({ inputRange: [0, 0.5, 1], outputRange: ["0deg", "90deg", "180deg"], }), }, ], }} />
这个项目的 repo 可以 在 GitHub 上找到。
在本文中,我们探索了使用 Three.js 在 React Native 中创建 3D 内容。 Three.js 支持在 React Native 环境中渲染 3D 模型。 当与 Animated API 结合使用时,这些工具可以为我们提供额外的灵活性,使我们能够为我们的用户构建更流畅、更有吸引力的视图。 这只是对可以使用 Animated API 执行的惊人动画的一种体验。
希望这篇文章可以作为未来开发人员创建出色用户体验的探索性指南。
成千上万的工程和产品团队使用 LogRocket 来减少了解其 React Native 应用程序中技术和可用性问题的根本原因所需的时间。 使用 LogRocket,您将减少与客户来回对话的时间,并消除无休止的故障排除过程。 LogRocket 让您可以花更多时间构建新事物,而减少修复错误的时间。