Home > Enterprise >  Animating a paginator indicator into a button
Animating a paginator indicator into a button

Time:03-09

I have been developing a personal application to build a finance app. At the moment I'm creating an Onboarding screen, with is successfully working. Although I want to add some styles to it, I have created an animated paginator, but I want to make the last page indicator turn into a Touchable button.
At the moment the paginator looks like this: enter image description here

When it reaches the last one: enter image description here

I want that last animation turn into a button.
This is my code for Paginator:

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>
    );
}

Styles:

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};
`;

I tried implementing this logic, but there was no animation. Of course.
I want it to turn into something like this: enter image description here This is the page with calls the paginator:

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>
    );
}

Formation mistake:
enter image description here

CodePudding user response:

The key points were:

  1. When we scroll from n-1th to nth page,
    1. All indicators except nth need to be adjusted. The adjustment could be either of
      1. Shrink content margin of all other indicators to 0 width. ( preferred )
      2. Move all indicators to left by calculated amount.
    2. The nth element should grow to occupy full width. The contents should also change opacity from 0 to 1.

With this points in mind, it should be easy to understand following changes in Paginator code.

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>
  );
}
  • Related