使用react native制作可折叠的粘性标题动画

Recently I had to develop a collapsible or multi-layered sticky header in React Native for a project, I spent some time looking at how I would go about implementing, and at that time I would have preferred to have found a tutorial on achieving the same. Since I couldn’t find one as per my requirements. I decided to write a tutorial on the same.

最近,我不得不在React Native中为一个项目开发一个可折叠或多层的粘性标头,我花了一些时间看我将如何实现,而那时我宁愿找到一个关于实现相同目标的教程。 由于我找不到符合我要求的产品。 我决定在同一篇文章上写一个教程。

我们正在建造什么 (What we are building)

Image for post

The header hides and reveals based on the direction of the scroll and once the scrolling is stopped, the header conveniently snaps to the closest state i.e either half-hidden or fully revealed. This effect can be seen in apps like WhatsApp, Youtube, Telegram, etc. We’d be using React Native’s Animated API to build this so let’s get started!

页眉根据滚动方向隐藏和显示,一旦滚动停止,页眉便会方便地捕捉到最接近的状态,即半隐藏或完全显示。 可以在WhatsApp,Youtube,Telegram等应用程序中看到这种效果。我们将使用React Native的Animated API来构建此效果,所以让我们开始吧!

I made a starter template that would save us some time by focusing on the topic of animations in this article. So I recommend you to clone the repository and follow along :)

我制作了一个入门模板 可以通过重点关注动画主题来节省一些时间。 因此,我建议您克隆存储库并遵循:)

设置入门模板的说明 (Instructions to setup the starter template)

  1. Clone the repository

    克隆存储库
git clone https://github.com/frzkn/rn-collapsible-header

2. Installing dependencies

2.安装依赖项

cd rn-collapsible-header && yarn

3. Switch to starter branch

3.切换到入门分支

git checkout starter

4. Starting the metro bundler

4.启动地铁打包机

yarn start

4. Starting it on a device (Android in my case)

4.在设备上启动(在我的情况下为Android)

yarn android

N

ñ

打开App.js并查看已经为我们完成的工作 (Open up App.js & see what’s already done for us)

import React, {useRef} from 'react';
import {
  FlatList,
  View,
  SafeAreaView,
  StatusBar,
  StyleSheet,
} from 'react-native';
import Header from './components/Header';
import ListItem from './components/ListItem';
import {generateData} from './data';


const headerHeight = 58 * 2;


const App = () => {
  const data = generateData(25);
  const ref = useRef(null);


  return (
    <SafeAreaView style={styles.container}>
      <StatusBar backgroundColor="#1c1c1c" style="light" />
      <View style={[styles.header]}>
        <Header {...{headerHeight}} />
      </View>
      <FlatList
        scrollEventThrottle={16}
        contentContainerStyle={{paddingTop: headerHeight}}
        ref={ref}
        data={data}
        renderItem={ListItem}
        keyExtractor={(item, index) => `list-item-${index}-${item.color}`}
      />
    </SafeAreaView>
  );
};


const styles = StyleSheet.create({
  header: {
    position: 'absolute',
    backgroundColor: '#1c1c1c',
    left: 0,
    right: 0,
    width: '100%',
    zIndex: 1,
  },
  subHeader: {
    height: headerHeight / 2,
    width: '100%',
    paddingHorizontal: 10,
  },
  container: {
    flex: 1,
    backgroundColor: '#000',
  },
});


export default App;

App.js

App.js

Image for post

我们要做什么的概述 (Overview of what we’re going to do)

  1. Translating the header based on the scroll events

    根据滚动事件翻译标题
  2. Implementing snap to fully expanded or half expanded state

    将快照实施为完全扩展半扩展状态

We have a FlatList which renders static data along with an app header. Upon scrolling, we can see that it doesn’t respond to our scroll events and just stays expanded. So let’s begin with that.

我们有一个FlatList,它呈现静态数据以及应用程序标头。 滚动时,我们可以看到它不响应滚动事件,只是保持扩展状态。 因此,让我们开始吧。

根据滚动事件翻译标题 (Translating the header based on the scroll events)

I’ve broken down the following into 5 step process as follows,

我将以下步骤分为5个步骤,

1. Converting our Components to Animated Components

1.将我们的组件转换为动画组件

Currently, we are using FlatList which React native provides us with. To be able to use animations the Animation API requires us to wrap our components with an createAnimatedComponent function which applies all the animation properties to our normal component, Example below.

当前,我们正在使用React native提供给我们的FlatList。 为了能够使用动画, Animation API要求我们使用createAnimatedComponent函数包装我们的组件,该函数将所有动画属性应用于我们的常规组件,如下面的示例。

const AnimatedFlatList = createAnimatedComponent(FlatList);

For most commonly used components such as View, FlatList, etc. Animated API already provides us these components so we can start using these components directly instead. Firstly import Animated from react-native

对于最常用的组件,例如View,FlatList等。AnimatedAPI已经为我们提供了这些组件,因此我们可以直接开始使用这些组件。 首先从react-native导入Animated

import {Animated, ... } from ‘react-native’

Replace our FlatList with Animated.FlatList and the View which wraps the Header component with Animated.View

Animated.FlatList并与Animated.View包裹头组件的替换查看我们的FlatList

Now, these components are ready to handle animations and animated events.

现在,这些组件已准备就绪,可以处理动画和动画事件。

2. Adding an onScroll Event

2.添加一个onScroll事件

The idea is to extract the progress of how much the user has scrolled in the Y direction. So to store this value, let’s create an animated value called scrollY

这个想法是提取用户在Y方向上滚动了多少的进度。 因此,要存储此值,我们创建一个名为scrollY的动画值

const scrollY = useRef(new Animated.Value(0));

FlatList takes an onScroll prop which is fired continuously as the FlatList is scrolled, we pass a function handle scroll which is an animated event. This animated event does one job to listen for changes in scrollY on the scroll and assign it to the scrollY variable.

FlatList带有一个onScroll道具,随着FlatList滚动,该道具会连续触发,我们传递了一个函数手柄滚动,它是一个动画事件。 此动画事件完成一项工作,以侦听滚动条上的scrollY并将其分配给scrollY变量。

const handleScroll = Animated.event(
[
{
nativeEvent: {
contentOffset: {y: scrollY.current},
},
},
],
{
useNativeDriver: true,
},
);

Notice the useNativeDriver key, this is very important as that makes sure the Animation is running on the Native UI Thread and not blocking any Javascript operations. This is well explained in the blog on the react-native’s website. But in a nutshell, it makes your animations achieve 60fps even on lower-end devices.

注意useNativeDriver键,这非常重要,因为这确保了Animation在本机UI线程上运行并且不阻止任何Javascript操作。 在react-native网站上博客中对此进行了很好的解释。 简而言之,即使在低端设备上,它也可以使动画达到60fps。

Now we’d get Value starting from 0 to the total height of the FlatList as the user scrolls. To clamp this value in a range, we’d use a function called diffClamp

现在,我们会得到价值从0开始到FlatList当用户滚动的总高度。 为了将该值限制在一个范围内,我们将使用一个称为diffClamp的函数

3. diffClamping our values

3.区分我们的价值观

As the name suggests the function does two things, Clamping the values between a range and returning a new value relative to the previous value. That means for a range of 0 to 10, Given the value x, we get 5 for the first time. It cannot be said that the subsequent calls with the same input will return the same output.

顾名思义,该函数可做两件事:将值固定在范围内,并返回相对于先前值的新值。 这意味着对于0到10的范围,给定值x,我们第一次得到5。 不能说具有相同输入的后续调用将返回相同的输出。

const scrollYClamped = diffClamp(scrollY.current, 0, headerHeight);

This will return us a value between the range of 0 to the height of the header which is 58 * 2. Lastly, we need to interpolate this value to a range of ( -headerHeight / 2) i.e. half expanded state and 0 i.e. fully expanded state.

这将返回一个介于0到标题的高度之间的值,即58 *2。最后,我们需要将该值插值到(-headerHeight / 2)的范围,即半展开状态和0,即完全展开州。

4. Interpolating the value

4.插值

The idea is to translate the header in the negative Y direction as per the scroll position since we want to translate it up to the point of -( headerHeight/ 2) that is one of the output range along with 0, which means the header is not translated at all.

这个想法是按照滚动位置在负Y方向上平移标题,因为我们想将其平移到输出范围之一的-(headerHeight / 2)点 ,即0。完全不翻译。

Animated API provides us a convenient function called interpolate which takes an array of input range and output range and interpolates the input value to the output value. Pretty straight forward right?

动画API为我们提供了一个方便的函数,称为插值(Interpolate),该函数接受输入范围和输出范围的数组并将输入值插值到输出值。 很简单吧?

const translateY = scrollYClamped.interpolate({
inputRange: [0, headerHeight],
outputRange: [0, -(headerHeight / 2)],
});const translateYNumber = useRef();translateY.addListener(({value}) => {
translateYNumber.current = value;
});

We call it translateY as it will be the final calculated value required to drive our animation.

我们将其称为translateY,因为它将是驱动动画所需的最终计算值。

We also add a listener to this value as we’d be requiring the Animated.Value in the type of Number in the next part.

我们还将在此值上添加一个侦听器,因为在下一部分中我们将需要Animated.Value类型为Number的类型。

5. Last piece of the puzzle

5.最后的难题

Head down to Animated.View the wrapper we came across earlier and add the following style

前往Animated 。查看我们之前遇到的包装器并添加以下样式

<Animated.View style={[styles.header, {transform: [{translateY}]}]}>
<Header {…{headerHeight}} />
</Animated.View>

And that’s it, try scrolling and see the header smoothly react to scroll events. While it looks good but it is not perfect. from a usability point of view if the user stops scrolling while the header is between fully expanded and half expanded state. This becomes a bad UX as the user has to scroll more towards the downwards direction to fully reveal the contents of the first half of the header.

就是这样,尝试滚动并查看标题对滚动事件的平滑响应。 虽然看起来不错,但并不完美。 从可用性的角度来看,如果用户在标头处于完全展开状态和半展开状态之间时停止滚动。 由于用户必须向下滚动更多滚动才能完全显示标题前半部分的内容,因此这将成为不良的UX。

Image for post
It doesn’t snap!
它不会折断!

将快照实施为完全扩展或半扩展状态 (Implementing snap to fully expanded or half expanded state)

Before diving into our approach I recommend you to see how other apps tackle this problem for yourself to have a better idea. Let’s take a look at a few together.

在深入探讨我们的方法之前,建议您先看看其他应用如何自己解决此问题,以获得更好的主意。 让我们一起看几个。

  • WhatsApp Messenger example

    WhatsApp Messenger示例

Image for post
Header slides down
标题向下滑动

We can see that for when the header is in between fully expanded and half expanded state the header and only header is snapped either to the top or bottom. This approach works but I went on to see more apps and how their approach to the problem

我们可以看到,当标头处于完全展开状态和半展开状态之间时,标头和仅标头会对齐到顶部或底部。 这种方法可行,但我继续查看更多应用程序以及它们如何解决问题

  • Telegram Messenger example

    电报信使示例

Image for post
FlatList Slides up
FlatList向上滑动

This one is pretty interesting as opposed to only animating the header, the FlatList is scrolled to either up or down for the header to reach to its two states.

与仅设置标题动画相反,这很有趣,FlatList向上或向下滚动以使标题到达其两种状态。

I like this simplistic approach to the problem and react-native makes it easy to implement this as well. The idea is to see if the header is in semi-visible state, if so then scroll the FlatList enough so that the header gets in either of our two desired states. So let us implement that next

我喜欢这种简单的方法来解决问题,而本机React也使实施起来很容易。 想法是查看标头是否处于半可见状态,如果是,则滚动FlatList以便标头进入我们所需的两个状态之一。 所以让我们接下来实现

Adding an onMomentumScrollEnd Event

添加一个onMomentumScrollEnd事件

Unlike the onScroll event, the onMomemtumScrollEnd event only fires once the user stops scrolling. so let’s start by writing a handleSnap function and use the ref that is assigned to our FlatList.

与onScroll事件不同, onMomemtumScrollEnd事件仅在用户停止滚动后才触发。 因此,让我们从编写handleSnap函数开始,并使用分配给FlatList的ref。

const handleSnap = ({nativeEvent}) => {
    const offsetY = nativeEvent.contentOffset.y;
    if (
      !(
        translateYNumber.current === 0 ||
        translateYNumber.current === -headerHeight / 2
      )
    ) {
      if (ref.current) {
        ref.current.scrollToOffset({
          offset:
            getCloser(translateYNumber.current, -headerHeight / 2, 0) ===
            -headerHeight / 2
              ? offsetY + headerHeight / 2
              : offsetY - headerHeight / 2,
        });
      }
    }
  };
<Animated.FlatList
scrollEventThrottle={16}
contentContainerStyle={{paddingTop: headerHeight}}
onScroll={handleScroll}
ref={ref} onMomentumScrollEnd={handleSnap} data={data}
renderItem={ListItem}
keyExtractor={(item, index) => `list-item-${index}-${item.color}`}
/>

This block is pretty self explanatory as well, the translateYNumber is the Animated.Value converted to the type Number. We use this value to check if the header is in the desired location, if not then we scroll the FlatList enough to make sure that the header snaps to the desired location.

该块也很容易说明,translateYNumber是转换为Number类型的Animated.Value。 我们使用这个值来检查,如果标题是在所需的位置,如果没有的话,我们滚动FlatList 足以确保头捕捉到所需的位置。

Note: Theoritically we shouldn’t be needing to add an offsetY in the scrollToOffset method’s offset key but this seems to be bug where the offset is not applied automatically.

ñOTE:Theoritically我们不应该需要添加在scrollToOffset方法的偏移关键的OFFSETY但这似乎是臭虫的偏移不会自动应用。

Image for post

And yay! our results are much better than before, it shows us something so simple can make or break the user experience. I am pretty happy with the results and that’s the end of it :)

是的! 我们的结果比以前要好得多,它向我们展示了如此简单的东西可以改变或破坏用户体验。 我对结果非常满意,到此为止:)

您如何贡献? (How Can You Contribute?)

  • By making a PR on the repository as I know there are a lot better ways to do the same and would love to see your approaches.

    据我所知,通过对资源库进行PR,有很多更好的方法可以做到这一点,并且很乐意看到您的方法。

  • Connect with me on Twitter or Linkedin or GitHub.

    TwitterLinkedinGitHub上与我联系。

  • Star the Github repository.

    Github存储库加注星标。

  • Follow me for more related content.

    关注我以获取更多相关内容。
  • Lastly share the article with your friends!

    最后与您的朋友分享文章!

If you liked this article, Show your support by clapping 👏 on this article as this is my first article and that’d motivate me to write more often.

如果您喜欢这篇文章,请通过在本文上拍击 Show表示支持,因为这是我的第一篇文章,这激励着我更加频繁地写作。

翻译自: https://medium.com/@farazrk001/making-a-collapsible-sticky-header-animations-with-react-native-6ad7763875c3

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值