Twitter的iOS应用程序有一个我非常喜欢的加载动画。
应用程序准备就绪后,Twitter的徽标会愉快地展开,显示应用程序。
我想弄清楚如何使用React Native重新创建此加载动画。
为了理解如何构建它,我首先必须了解加载动画的不同部分。减慢动画速度是查看细微差别的最简单方法。
这里有一些主要部分,我们需要弄清楚如何构建它们。
- 缩放小鸟。
- 随着小鸟的长大,显示下面的应用程序
- 在动画结束时稍微缩小应用程序
我花了相当长的时间才弄清楚如何制作这个动画。
我最初错误地假设蓝色背景和Twitter小鸟是应用程序顶部的一层,并且随着小鸟的长大,它会变得透明,从而显示下面的应用程序。这种方法行不通,因为Twitter小鸟变得透明会显示蓝色层,而不是下面的应用程序!
幸运的是,亲爱的读者,您不必像我一样经历同样的挫折。您可以通过这个不错的教程直接跳到精华部分!
正确的方法
在我们开始编写代码之前,了解如何分解它非常重要。为了帮助您可视化此效果,我在CodePen(嵌入在几个段落中)中重新创建了它,以便您可以交互式地查看不同的图层。
此效果主要有三个图层。第一个是蓝色背景层。即使这似乎出现在应用程序的顶部,它实际上是在后面。
然后我们有一个纯白色的图层。最后,最前面是我们的应用程序。
此动画的主要技巧是使用Twitter徽标作为mask
,并掩盖应用程序和白色图层。我不会深入探讨掩盖的细节,有很多资源在线可以帮助您了解它。
在此上下文中,掩盖的基本原理是拥有图像,其中蒙版的不透明像素显示它们正在掩盖的内容,而蒙版的透明像素则隐藏它们正在掩盖的内容。
我们使用Twitter徽标作为蒙版,并使其掩盖两个图层:纯白色图层和应用程序图层。
为了显示应用程序,我们将蒙版放大,直到它大于整个屏幕。
在蒙版放大时,我们淡入应用程序图层的透明度,显示应用程序并隐藏其后面的纯白色图层。为了完成效果,我们让应用程序图层从大于1的比例开始,并在动画结束时缩小到1。然后,我们隐藏非应用程序图层,因为它们将永远不会再被看到。
俗话说,一张图片胜过千言万语。交互式可视化值多少个字?使用“下一步”按钮浏览动画。显示图层可以让您从侧面视角进行查看。网格有助于可视化透明图层。
现在,关于React Native
好了。现在我们知道了要构建什么以及动画的工作原理,我们可以开始编写代码了——您真正来到这里的目的。
这个难题的主要部分是MaskedViewIOS,这是一个React Native核心组件。
import {MaskedViewIOS} from 'react-native';
<MaskedViewIOS maskElement={<Text>Basic Mask</Text>}>
<View style={{backgroundColor: 'blue'}} />
</MaskedViewIOS>;
MaskedViewIOS
接受属性maskElement
和children
。子元素由maskElement
掩盖。请注意,蒙版不需要是图像,它可以是任何任意视图。上面示例的行为将是渲染蓝色视图,但它仅在maskElement
中的“Basic Mask”文字所在位置可见。我们只是创建了复杂的蓝色文本。
我们要做的是渲染我们的蓝色层,然后在其顶部使用Twitter徽标渲染我们掩盖的应用程序和白色层。
{
fullScreenBlueLayer;
}
<MaskedViewIOS
style={{flex: 1}}
maskElement={
<View style={styles.centeredFullScreen}>
<Image source={twitterLogo} />
</View>
}>
{fullScreenWhiteLayer}
<View style={{flex: 1}}>
<MyApp />
</View>
</MaskedViewIOS>;
这将为我们提供下面看到的图层。
现在进行动画部分
我们拥有使之工作所需的所有组件,下一步是为它们制作动画。为了使此动画感觉良好,我们将利用React Native的Animated API。
Animated允许我们以声明方式在JavaScript中定义动画。默认情况下,这些动画在JavaScript中运行,并在每一帧告诉本机层要进行哪些更改。即使JavaScript会尝试在每一帧更新动画,但它可能无法足够快地执行此操作,并且会导致出现丢帧(卡顿)。这不是我们想要的!
Animated具有特殊的行为,允许您在没有卡顿的情况下获得动画。Animated有一个名为useNativeDriver
的标志,它在动画开始时将您的动画定义从JavaScript发送到本机,允许本机端处理对动画的更新,而无需在每一帧都来回切换到JavaScript。useNativeDriver
的缺点是您只能更新特定的一组属性,主要是transform
和opacity
。您无法使用useNativeDriver
为诸如背景颜色之类的属性制作动画,至少目前还不行——我们将在以后添加更多内容,当然,您也可以随时为项目所需的属性提交PR,从而使整个社区受益😀。
由于我们希望此动画流畅,因此我们将在此限制范围内工作。要更深入地了解useNativeDriver
在幕后如何工作,请查看我们发布它的博文。
分解我们的动画
我们的动画有4个组成部分
- 放大小鸟,显示应用程序和纯白色图层
- 淡入应用程序
- 缩小应用程序
- 完成后隐藏白色图层和蓝色图层
使用Animated,有两种主要方法可以定义动画。第一种是使用Animated.timing
,它允许您准确说明动画将运行多长时间,以及平滑运动的缓动曲线。另一种方法是使用基于物理的api,例如Animated.spring
。使用Animated.spring
,您可以指定弹簧的摩擦力和张力等参数,并让物理运行动画。
我们希望同时运行多个动画,这些动画彼此密切相关。例如,我们希望应用程序在蒙版处于中间显示状态时开始淡入。由于这些动画密切相关,我们将使用Animated.timing
和单个Animated.Value
。
Animated.Value
是本机值的包装器,Animated使用它来了解动画的状态。通常,您只希望为一个完整的动画使用其中一个。大多数使用Animated的组件会将该值存储在状态中。
由于我将此动画视为在完整动画的不同时间点发生的步骤,因此我们将Animated.Value
从0开始,表示完成0%,并将我们的值结束为100,表示完成100%。
我们的初始组件状态如下所示。
state = {
loadingProgress: new Animated.Value(0),
};
当我们准备好开始动画时,我们会告诉Animated将此值动画设置为100。
Animated.timing(this.state.loadingProgress, {
toValue: 100,
duration: 1000,
useNativeDriver: true,
}).start();
然后,我尝试估算动画的不同部分以及我希望它们在动画的不同阶段具有的值。下表列出了动画的不同部分,以及我认为随着时间的推移,它们在不同点应该具有的值。
Twitter小鸟蒙版应从比例1开始,并在向上放大之前变小。因此,在动画进行到10%时,它应该具有0.8的比例值,然后在最后放大到70。老实说,选择70是相当随意的,它需要足够大才能使小鸟完全显示屏幕,而60不够大😀。关于这部分有趣的一点是,数字越大,看起来增长的速度就越快,因为它必须在相同的时间内到达那里。这个数字需要一些反复试验才能使这个徽标看起来不错。不同尺寸的徽标/设备将需要不同的最终比例,以确保完全显示整个屏幕。
应用程序应该保持不透明一段时间,至少在Twitter徽标变小的时候。根据官方动画,我希望在小鸟放大到中途时开始显示它,并尽快完全显示它。因此,在15%时我们开始显示它,在整个动画进行到30%时,它完全可见。
应用的缩放比例从 1.1 开始,并在动画结束时缩放到其常规比例。
现在,我们来看代码。
我们在上面做的本质上是将动画进度百分比的值映射到各个片段的值。我们使用 .interpolate
通过 Animated 来做到这一点。我们使用基于 this.state.loadingProgress
的插值值创建了 3 个不同的样式对象,每个动画片段一个。
const loadingProgress = this.state.loadingProgress;
const opacityClearToVisible = {
opacity: loadingProgress.interpolate({
inputRange: [0, 15, 30],
outputRange: [0, 0, 1],
extrapolate: 'clamp',
}),
};
const imageScale = {
transform: [
{
scale: loadingProgress.interpolate({
inputRange: [0, 10, 100],
outputRange: [1, 0.8, 70],
}),
},
],
};
const appScale = {
transform: [
{
scale: loadingProgress.interpolate({
inputRange: [0, 100],
outputRange: [1.1, 1],
}),
},
],
};
现在我们有了这些样式对象,我们可以在渲染文章前面提到的视图片段时使用它们。请注意,只有 Animated.View
、Animated.Text
和 Animated.Image
才能使用使用 Animated.Value
的样式对象。
const fullScreenBlueLayer = (
<View style={styles.fullScreenBlueLayer} />
);
const fullScreenWhiteLayer = (
<View style={styles.fullScreenWhiteLayer} />
);
return (
<View style={styles.fullScreen}>
{fullScreenBlueLayer}
<MaskedViewIOS
style={{flex: 1}}
maskElement={
<View style={styles.centeredFullScreen}>
<Animated.Image
style={[styles.maskImageStyle, imageScale]}
source={twitterLogo}
/>
</View>
}>
{fullScreenWhiteLayer}
<Animated.View
style={[opacityClearToVisible, appScale, {flex: 1}]}>
{this.props.children}
</Animated.View>
</MaskedViewIOS>
</View>
);
耶!我们现在已经让动画片段看起来像我们想要的那样了。现在我们只需要清理我们永远不会再看到的蓝色和白色图层。
为了知道何时可以清理它们,我们需要知道动画何时完成。幸运的是,在我们调用 Animated.timing
的地方,.start
接受一个可选的回调函数,该函数在动画完成后运行。
Animated.timing(this.state.loadingProgress, {
toValue: 100,
duration: 1000,
useNativeDriver: true,
}).start(() => {
this.setState({
animationDone: true,
});
});
现在我们在 state
中有一个值来判断我们是否完成了动画,我们可以修改我们的蓝色和白色图层来使用它。
const fullScreenBlueLayer = this.state.animationDone ? null : (
<View style={[styles.fullScreenBlueLayer]} />
);
const fullScreenWhiteLayer = this.state.animationDone ? null : (
<View style={[styles.fullScreenWhiteLayer]} />
);
瞧!我们的动画现在可以工作了,并且在动画完成后我们清理了未使用的图层。我们已经构建了 Twitter 应用加载动画!
但是等等,我的动画不起作用!
不要担心,亲爱的读者。我也讨厌指南只给你代码片段而不给你完整的源代码。
此组件已发布到 npm 并托管在 GitHub 上,地址为 react-native-mask-loader。要在你的手机上试用它,可以 在 Expo 上访问。
- 这本 Gitbook 是一个很好的资源,可以帮助你阅读 React Native 文档后学习更多关于 Animated 的知识。
- 实际的 Twitter 动画似乎在结束时加快了蒙版的显示速度。尝试修改加载器以使用不同的缓动函数(或弹簧!)来更好地匹配这种行为。
- 蒙版的当前结束缩放比例是硬编码的,在平板电脑上可能无法显示整个应用。根据屏幕尺寸和图像尺寸计算结束缩放比例将是一个很棒的 PR。