2

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

Nilton Schumacher F
  • 814
  • 3
  • 13
  • 43

1 Answers1

1

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>
  );
}
Avinash Thakur
  • 1,640
  • 7
  • 16
  • Well, this was some damn logic. I will be testing it now! – Nilton Schumacher F Mar 08 '22 at 20:00
  • Array fill needs params, with would it be? – Nilton Schumacher F Mar 08 '22 at 20:05
  • @NiltonSchumacherF Updated answer. It could be anything since we're overriding it in `.map`. – Avinash Thakur Mar 08 '22 at 20:09
  • I got component expection error `Invariant Violation: [85,"RCTView",101,{"opacity":0,"backgroundColor":4283840211}] is not usable as a native method argument` – Nilton Schumacher F Mar 08 '22 at 20:13
  • Got this error too: `Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function` – Nilton Schumacher F Mar 08 '22 at 20:15
  • And two warnings on unhandled promises: `Possible Unhandled Promise Rejection (id: 0): Error: Exception in HostFunction: Malformed calls from JS: field sizes are different. [[63,63,63,0],[5,3,5,0],[[83,"RCTText",101,{"ellipsizeMode":"tail","allowFontScaling":true,"accessible":true,"color":4294967295}],[83,[79]],["ExpoKeepAwake",1,["ExpoKeepAwakeDefaultTag"],1364,1365]],679]` – Nilton Schumacher F Mar 08 '22 at 20:15
  • This is not happening to me in expo. You can try removing `opacity` and `backgroundColor` styles from ` – Avinash Thakur Mar 08 '22 at 20:35
  • It was the opacity, it is working fine, but the height got messed up, could you fix that? The image from the button is messed up when I'm not in the button page, it appears as a `n`. I will be accepting this answer than! – Nilton Schumacher F Mar 08 '22 at 20:53
  • Can you recreate it in an expo and share that ? I can't recreate it, don't even have issue with opacity. – Avinash Thakur Mar 08 '22 at 20:56
  • Strange, I will fix try to fix it, is there a way to make the button not 100% of width? Like 100px or another value? I will accept the answer and try to fix this my/self – Nilton Schumacher F Mar 08 '22 at 20:59
  • Could be difference in any dependency. – Avinash Thakur Mar 08 '22 at 21:02
  • Now it is working, I just needed to insert opacity at the end of backgroud, but there is a slite appearence on the dots, is there a fix to it? I have updated the last image – Nilton Schumacher F Mar 08 '22 at 21:15
  • I don't think opacity is working for your case. The text inside the button shouldn't be appearing like that. Can you recreate your issue on https://snack.expo.dev/ and provide me link ? – Avinash Thakur Mar 08 '22 at 21:43