将分页指示器动画化为按钮

Animating a paginator indicator into a button

我一直在开发个人应用程序来构建财务应用程序。目前我正在创建一个入职屏幕,并成功运行。虽然我想给它添加一些样式,我已经创建了一个动画分页器,但是我想让最后一页指示器变成一个可触摸的按钮。
目前分页器看起来像这样:

当它到达最后一个时:

我希望最后一个动画变成一个按钮。
这是我的分页器代码:

import React from 'react';
import { 
    Container,
    CurrentSelectedPageIndicator,
    ButtonContainer
} from './styles';
import { useWindowDimensions } from 'react-native';

interface PaginatorProps {
    data: any;
    scrollX: any;
    currentIndex: any;
}

export function Paginator({ data, scrollX, currentIndex }: PaginatorProps){

    const { width } = useWindowDimensions();

    return (
        <Container>
            {data.map((_: any, index: any) => {
                const inputRange = [(index - 1) * width, index * width, (index + 1) * width];

                let dotWidth = scrollX.interpolate({
                    inputRange,
                    outputRange: [10, 20, 10],
                    extrapolate: 'clamp'
                });

                const opacity = scrollX.interpolate({
                    inputRange,
                    outputRange: [0.3, 1, 0.3],
                    extrapolate: 'clamp'
                });     

                if (currentIndex.toString() === '2') {
                    dotWidth = scrollX.interpolate({
                        [1,2,3],
                        outputRange: [10, 20, 10],
                        extrapolate: 'clamp'
                    });
                }

                return <CurrentSelectedPageIndicator key={index.toString()} style={{ width: dotWidth, opacity }} />;
            })}
        </Container>
    );
}

样式:

import { RFValue } from "react-native-responsive-fontsize";
import styled from "styled-components/native";
import { Animated } from 'react-native';

export const Container = styled.View`
    flex-direction: row;
    height: ${RFValue(64)}px;
`;

export const CurrentSelectedPageIndicator = styled(Animated.View).attrs({
    shadowOffset: { width: 1, height: 3 }
})`
    shadow-color: ${({ theme }) => theme.colors.text_dark };
    elevation: 1;
  shadow-opacity: 0.3;
  shadow-radius: 1px;
    height: ${RFValue(10)}px;
    width: ${RFValue(10)}px;
    border-radius: 10px;
    background-color: ${({ theme }) => theme.colors.blue };
    margin-horizontal: ${RFValue(8)}px;
`;

export const ButtonContainer = styled(Animated.View)`
    width: 100%;
    height: ${RFValue(50)}px;
    background-color: ${({ theme }) => theme.colors.blue};
    border-radius: 10px;
    align-items: center;
    justify-content: center;
`;

export const ButtonTitle = styled.Text`
    font-family: ${({ theme }) => theme.fonts.medium};
    font-size: ${RFValue(14)}px;
    color: ${({ theme }) => theme.colors.shapeColor};
`;

我尝试实现这个逻辑,但是没有动画。当然可以。
我希望它变成这样的东西: 这是调用分页器的页面:

import React, { useState, useRef } from 'react';
import { 
    Container,
    FlatListContainer
} from './styles';
import {
    FlatList,
    Animated
} from 'react-native'
import OnboardingData from '../../utils/onboarding';
import { OnboardingItem } from '../../components/OnboardingItem';
import { Paginator } from '../../components/Paginator';

export function Onboarding(){

    const [currentIndex, setCurrentIndex] = useState(0);
    const scrollX = useRef(new Animated.Value(0)).current;
    const onboardingDataRef = useRef(null);

    const viewableItemsChanged = useRef(({ viewableItems }: any) => {
        setCurrentIndex(viewableItems[0].index);
    }).current;

    const viewConfig = useRef({ viewAreaCoveragePercentThreshold: 50 }).current;

    return (
        <Container>
            <FlatListContainer>
                <FlatList 
                    data={OnboardingData} 
                    renderItem={({ item }) => <OnboardingItem image={item.image} title={item.title} description={item.description}/>}
                    horizontal
                    showsHorizontalScrollIndicator={false}
                    pagingEnabled={true}
                    bounces={false}
                    keyExtractor={(item) => String(item.id)}
                    onScroll={Animated.event([{ nativeEvent: { contentOffset: { x: scrollX } }}], {
                        useNativeDriver: false
                    })}
                    scrollEventThrottle={32}
                    onViewableItemsChanged={viewableItemsChanged}
                    viewabilityConfig={viewConfig}
                    ref={onboardingDataRef}
                />
            </FlatListContainer>

            <Paginator data={OnboardingData} scrollX={scrollX} currentIndex={currentIndex}/>
        </Container>
    );
}

编队错误:

关键点是:

  1. 当我们从第n-1页滚动到第n页时,
    1. 除nth以外的所有指标都需要调整。调整可以是
      1. 将所有其他指标的内容+边距缩小为0宽度。 (首选)
      2. 将所有指标向左移动计算量。
    2. n 个元素应该增长到占据整个宽度。内容还应将不透明度从 0 更改为 1

考虑到这一点,Paginator 代码的以下更改应该很容易理解。

import React from 'react';
import {
  Container,
  CurrentSelectedPageIndicator,
  ButtonContainer,
  ButtonTitle,
} from './styles';
import { useWindowDimensions } from 'react-native';
import { RFValue } from 'react-native-responsive-fontsize';
interface PaginatorProps {
  data: any;
  scrollX: any;
  currentIndex: any;
}

const inactiveSize = RFValue(10)
const activeSize = RFValue(20)

export function Paginator({ data, scrollX, currentIndex }: PaginatorProps) {
  const { width } = useWindowDimensions();

  return (
    <Container>
      {data.map((_: any, index: any) => {
        const inputRange = Array(data.length)
          .fill(0)
          .map((_, i) => i * width);
        const isLastElement = index === data.length - 1;
        const widthRange = Array(data.length)
          .fill(inactiveSize)
          .map((v, i) => {
            if (i === data.length - 1) {
              if (isLastElement) return width;
              return 0;
            }
            if (i === index) return activeSize;
            return v;
          });
        // optionally, reduce the length of inputRange & widthRange
        // while loop may be removed
        let i = 0;
        while (i < inputRange.length - 1) {
          if (widthRange[i] === widthRange[i + 1]) {
            let toRemove = -1;
            if (i === 0) toRemove = i;
            else if (i === inputRange.length - 2) toRemove = i + 1;
            else if (
              i < inputRange.length - 2 &&
              widthRange[i] === widthRange[i + 2]
            )
              toRemove = i + 1;
            if (toRemove > -1) {
              inputRange.splice(toRemove, 1);
              widthRange.splice(toRemove, 1);
              continue;
            }
          }
          i++;
        }
        console.log(index, inputRange, widthRange);
        let height = inactiveSize;
        let buttonOpacity = 0;
        let dotWidth = scrollX.interpolate({
          inputRange,
          outputRange: widthRange,
          extrapolate: 'clamp',
        });

        const opacity = scrollX.interpolate({
          inputRange,
          outputRange: widthRange.map((v) => ( v? v >= activeSize ? 1 : 0.3: 0)),
          extrapolate: 'clamp',
        });

        if (isLastElement) {
          dotWidth = scrollX.interpolate({
            inputRange,
            outputRange: widthRange,
            extrapolate: 'clamp',
          });
          height = dotWidth.interpolate({
            inputRange: [inactiveSize, width],
            outputRange: [inactiveSize, RFValue(50)],
            extrapolate: 'clamp',
          });
          buttonOpacity = dotWidth.interpolate({
            inputRange: [inactiveSize, width],
            outputRange: [0, 1],
            extrapolate: 'clamp',
          });
        }
        const marginHorizontal = dotWidth.interpolate({
          inputRange: [0, inactiveSize],
          outputRange: [0, RFValue(8)],
          extrapolate: 'clamp',
        });

        return (
          <CurrentSelectedPageIndicator
            key={index.toString()}
            style={{ width: dotWidth, opacity, marginHorizontal, height }}>
            {isLastElement && (
              <ButtonContainer
                style={{ opacity: buttonOpacity, backgroundColor: '#5636D3' }}>
                <ButtonTitle style={{ color: 'white' }}>NEXT</ButtonTitle>
              </ButtonContainer>
            )}
          </CurrentSelectedPageIndicator>
        );
      })}
    </Container>
  );
}