Setup proyecto

Instalacion

  1. npx @react-native-community/cli@latest init Components
  2. Intalamos StackNav que lo vimos en este post pre instalacion y aca la instlacion en si

Como ya sos grande te dejo la documentacion

  1. Instalar los iconos , te dejo el post anterior aca y la documentacion oficial de los iconos aca

Estructura

src/
├── config/
│   └── theme/
│       └── theme.ts
├── presentation/
│   ├── assets/
│   ├── hooks/
│   ├── icons/
│   ├── inputs/
│   ├── navigation/
│   │   └── StackNav.tsx
│   ├── screens/
│   │   ├── alerts/
│   │   ├── animations/
│   │   └── home/
│   │       └── HomeScreen.tsx
├── switches/
├── ui/
└── App.tsx

En theme.ts

import { StyleSheet } from 'react-native';

export interface ThemeColors {
  primary: string;
  text: string;
  background: string;
  cardBackground: string;
  buttonTextColor: string;
}

export const colors: ThemeColors = {
  primary: '#5856D6',
  text: 'black',

  background: '#F3F2F7',
  cardBackground: 'white',
  buttonTextColor: 'white',
};

export const globalStyles = StyleSheet.create({
  title: {
    fontSize: 30,
    fontWeight: 'bold',
    color: colors.text,
  },
  subTitle: {
    fontSize: 20,
    fontWeight: 'bold',
    color: colors.text,
  },

  mainContainer: {
    flex: 1,
    backgroundColor: colors.background,
  },
  globalMargin: {
    paddingHorizontal: 20,
    flex: 1,
  },

  btnPrimary: {
    backgroundColor: colors.primary,
    borderRadius: 10,
    padding: 10,
    alignItems: 'center',
  },
  btnPrimaryText: {
    color: colors.text,
    fontSize: 16,
  },
});

En HomeScreen solo por ahora, tendremos el relleno del memu:

import React from 'react';
import { View, Text } from 'react-native';
import Icon from 'react-native-vector-icons/Ionicons';
export const menuItems = [
  // 01-animationMenuItems
  {
    name: 'Animation 101',
    icon: 'cube-outline',
    component: 'Animation101Screen',
  },
  {
    name: 'Animation 102',
    icon: 'albums-outline',
    component: 'Animation102Screen',
  },


  // 02-menuItems
  {
    name: 'Pull to refresh',
    icon: 'refresh-outline',
    component: 'PullToRefreshScreen',
  },
  {
    name: 'Section List',
    icon: 'list-outline',
    component: 'CustomSectionListScreen',
  },
  {
    name: 'Modal',
    icon: 'copy-outline',
    component: 'ModalScreen',
  },
  {
    name: 'InfiniteScroll',
    icon: 'download-outline',
    component: 'InfiniteScrollScreen',
  },
  {
    name: 'Slides',
    icon: 'flower-outline',
    component: 'SlidesScreen',
  },
  {
    name: 'Themes',
    icon: 'flask-outline',
    component: 'ChangeThemeScreen',
  },

  // 03- uiMenuItems
  {
    name: 'Switches',
    icon: 'toggle-outline',
    component: 'SwitchScreen',
  },
  {
    name: 'Alerts',
    icon: 'alert-circle-outline',
    component: 'AlertScreen',
  },
  {
    name: 'TextInputs',
    icon: 'document-text-outline',
    component: 'TextInputScreen',
  },
];

export const HomeScreen = () => {
  return (
    <View>
      <Text>HomeScreen</Text>
      <Text>
        <Icon name="rocket" size={30} color="#900" />

      </Text>
    </View>
  );
};

Componente Title

Lo vamos a usar asi:

...todo el menu que teniamos antes 

export const HomeScreen = () => {
  return (
    <View style={globalStyles.mainContainer}>
      <View style={globalStyles.globalMargin}>
        <ScrollView>
          <Title text="Holis" />
        </ScrollView>
      </View>
      <Text />
    </View>
  );
};

Lo creamos en presentation/ui/Title:

import React from 'react';
import { Text } from 'react-native';
import { colors, globalStyles } from '../../config/theme/theme';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
interface Props {
  text: string;
  safe?: boolean;
  white?: boolean;
}
export const Title = ({ text, safe, white = false }: Props) => {
  const { top } = useSafeAreaInsets();
  return (
    <Text style={{
      ...globalStyles.title,
      marginTop: safe ? top : 0,
      marginBottom: 10,
      color: white ? 'white' : colors.text,
    }}>{text}</Text>
  );
};

Lo creamos en presentation/ui/MenuItem:

import React from 'react';
import { StyleSheet, View, Text } from 'react-native';
import { Pressable } from 'react-native-gesture-handler';
import { colors } from '../../config/theme/theme';
import Icon from 'react-native-vector-icons/Ionicons';
import { useNavigation } from '@react-navigation/native';

interface Props {
  name: string;
  icon: string;
  component: string;
  isFirst?: boolean;
  isLast?: boolean;
}
export const MenuItem = ({ name, icon, component, isLast = false, isFirst = false }: Props) => {
  const navigation = useNavigation<any>();
  return (
    <Pressable onPress={() => navigation.navigate(component)}>
      <View style={{
        ...styles.container,
        backgroundColor: colors.cardBackground,
        ...(isFirst && { borderTopLeftRadius: 10, borderTopRightRadius: 10, paddingTop: 10 }),
        ...(isLast && { borderBottomLeftRadius: 10, borderBottomRightRadius: 10, paddingBottom: 10 }),
      }} >
        <Icon
          name={icon}
          size={25}
          style={{ marginRight: 10 }}
          color={colors.primary} />
        <Text style={{ color: colors.text }}>{name}</Text>
        <Icon
          name="chevron-forward-outline"
          size={25}
          style={{ marginLeft: 'auto' }}
          color={colors.primary} />
      </View>
    </Pressable>
  );
};

const styles = StyleSheet.create({
  container: {
    flexDirection: 'row',
    alignItems: 'center',
    paddingHorizontal: 10,
    paddingVertical: 5,
  },
});

Por ahora dejaremos const navigation = useNavigation<any>(); con el tipo any, porque aun no creamos los componentes hacia los que navegaremos, pero ya lo vamos a tipar.

El HomeScreen lo dejamos asi :

import React from 'react';
import { View, Text, ScrollView } from 'react-native';
import { globalStyles } from '../../config/theme/theme';
import { Title } from '../ui/ Title';
import { MenuItem } from '../ui/MenuItem';
export const animationItems = [
  // 01-animationMenuItems
  {
    name: 'Animation 101',
    icon: 'cube-outline',
    component: 'Animation101Screen',
  },
  {
    name: 'Animation 102',
    icon: 'albums-outline',
    component: 'Animation102Screen',
  },

];
export const menuItems = [

  // 02-menuItems
  {
    name: 'Pull to refresh',
    icon: 'refresh-outline',
    component: 'PullToRefreshScreen',
  },
  {
    name: 'Section List',
    icon: 'list-outline',
    component: 'CustomSectionListScreen',
  },
  {
    name: 'Modal',
    icon: 'copy-outline',
    component: 'ModalScreen',
  },
  {
    name: 'InfiniteScroll',
    icon: 'download-outline',
    component: 'InfiniteScrollScreen',
  },
  {
    name: 'Slides',
    icon: 'flower-outline',
    component: 'SlidesScreen',
  },
  {
    name: 'Themes',
    icon: 'flask-outline',
    component: 'ChangeThemeScreen',
  },
];

export const uiItems = [
  // 03- uiMenuItems
  {
    name: 'Switches',
    icon: 'toggle-outline',
    component: 'SwitchScreen',
  },
  {
    name: 'Alerts',
    icon: 'alert-circle-outline',
    component: 'AlertScreen',
  },
  {
    name: 'TextInputs',
    icon: 'document-text-outline',
    component: 'TextInputScreen',
  }];

export const HomeScreen = () => {
  return (
    <View style={globalStyles.mainContainer}>
      <View style={globalStyles.globalMargin}>
        <ScrollView>

          <Title text="Opciones de menu" />
          {
            animationItems.map((item, index) => (
              <MenuItem
                key={item.component}
                {...item}
                //para calcular el border radius
                isFirst={index === 0}
                isLast={index === menuItems.length - 1}
              />
            ))
          }
          <View style={{ height: 10 }} />
          {
            menuItems.map((item, index) => (
              <MenuItem
                key={item.component}
                {...item}
                //para calcular el border radius
                isFirst={index === 0}
                isLast={index === menuItems.length - 1}
              />
            ))
          }

          <View style={{ height: 10 }} />
          {
            uiItems.map((item, index) => (
              <MenuItem
                key={item.component}
                {...item}
                //para calcular el border radius
                isFirst={index === 0}
                isLast={index === menuItems.length - 1}
              />
            ))
          }

        </ScrollView>
      </View>
      <Text />
    </View>
  );
};

Animated Api

Documentacion oficial

Hagamos unas animaciones en Animation101Screen, tenes que poner la ruta en StackNav asi podes acceder desde el menu que creamos antes.

import { Animated, Pressable, StyleSheet, Text, View } from 'react-native';
import React, { useRef } from 'react';
import { colors } from '../../../config/theme/theme';

const Animation101Screen = () => {

  const animatedOpacity = useRef(new Animated.Value(0)).current;

  const fadeIn = () => {
    Animated.timing(animatedOpacity, {
      toValue: 1,
      duration: 300,
      useNativeDriver: true, //acelaracion por hardware
    }).start(() => console.log('animation end'));
  };

  const fadeOut = () => {
    Animated.timing(animatedOpacity, {
      toValue: 0,
      duration: 300,
      useNativeDriver: true, //acelaracion por hardware
    }).start(() => console.log('animation end'));
  };

  return (
    <View style={styles.container}>
      <Animated.View style={[
        styles.purpleBox,
        {
          opacity: animatedOpacity,
        },
      ]} />
      <Pressable onPress={fadeIn} style={{ marginTop: 10 }}>
        <Text>FadeIN</Text>
      </Pressable>

      <Pressable onPress={fadeOut} style={{ marginTop: 10 }}>
        <Text>Fadeou</Text>
      </Pressable>
    </View >
  );
};

export default Animation101Screen;

const styles = StyleSheet.create({
  container: {
    justifyContent: 'center',
    flex: 1,
    alignItems: 'center',
  },
  purpleBox: {
    backgroundColor: colors.primary,
    width: 150,
    height: 150,
  },
});

Como el valor que va a cambiar no se va a ver en pantalla usamos useRef

const animatedOpacity = useRef(new Animated.Value(0)).current;

Lo unicializamos en 0 , esto quiere decir que el valor de opacity es 0

const fadeIn = () => {
    Animated.timing(animatedOpacity, {
      toValue: 1,
      duration: 300,
      useNativeDriver: true, //acelaracion por hardware
    }).start(() => console.log('animation end'));
  };

Para modificar el valor de un atributo seteado con Animated tenemso que usar el objeto Animated.timing().

Adicionalmente podemos usar start() para detectar el final de la animacion y disparar otra funcion pero tambien es necesaria para ejecutar la actual.

<Animated.View style={[
        styles.purpleBox,
        {
          opacity: animatedOpacity,
        },
      ]} />

Los elementos que sean animados tienen que ejecutarse como porpiedades del objeto Animated sino se nos mostrara un error, o la pantalla en blanco.

Bounce

Hagamos un efecto mas copado :

import { Animated, Easing, Pressable, StyleSheet, Text, View } from 'react-native';
import React, { useRef } from 'react';
import { colors } from '../../../config/theme/theme';

const Animation101Screen = () => {

  const animatedOpacity = useRef(new Animated.Value(0)).current;
  const animatedTop = useRef(new Animated.Value(-100)).current;

  const fadeIn = () => {
    Animated.timing(animatedTop, {
      toValue: 0,
      duration: 700,
      useNativeDriver: true,
      easing: Easing.elastic(1),
    }).start(() => console.log('animation end'));

    Animated.timing(animatedOpacity, {
      toValue: 1,
      duration: 300,
      useNativeDriver: true, //acelaracion por hardware
    }).start();
  };

  const fadeOut = () => {
    Animated.timing(animatedOpacity, {
      toValue: 0,
      duration: 300,
      useNativeDriver: true, //acelaracion por hardware
    }).start(() => animatedTop.resetAnimation());
  };


  return (
    <View style={styles.container}>
      <Animated.View style={[
        styles.purpleBox,
        {
          opacity: animatedOpacity,
          transform: [{ translateY: animatedTop }],
        },
      ]} />
      <Pressable onPress={fadeIn} style={{ marginTop: 10 }}>
        <Text>FadeIN</Text>
      </Pressable>

      <Pressable onPress={fadeOut} style={{ marginTop: 10 }}>
        <Text>Fadeou</Text>
      </Pressable>
    </View >
  );
};

export default Animation101Screen;

const styles = StyleSheet.create({
  container: {
    justifyContent: 'center',
    flex: 1,
    alignItems: 'center',
  },
  purpleBox: {
    backgroundColor: colors.primary,
    width: 150,
    height: 150,
  },
});

Dentro de FadeIN agregamos un efecto elastico

Animated.timing(animatedTop, {
    toValue: 0,
    duration: 700,
    useNativeDriver: true,
    easing: Easing.elastic(1),
  }).start(() => console.log('animation end'));

Podemos resetaer la animacion con animatedTop.resetAnimation() en fadeOut

const fadeOut = () => {
    Animated.timing(animatedOpacity, {
      toValue: 0,
      duration: 300,
      useNativeDriver: true, //acelaracion por hardware
    }).start(() => animatedTop.resetAnimation());
  };

Para terminar de lograr que el efecto funcione,es usar transform:transition de esta manera:

<Animated.View style={[
        styles.purpleBox,
        {
          opacity: animatedOpacity,
          transform: [{ translateY: animatedTop }],
        },
      ]} />

CustomHook useAnimation

import { useRef } from 'react';
import { Animated, Easing } from 'react-native';

export const useAnimation = () => {

  const animatedOpacity = useRef(new Animated.Value(0)).current;
  const animatedTop = useRef(new Animated.Value(-100)).current;

  const fadeIn = ({ duration = 300, toValue = 1, callback = () => { } }) => {

    Animated.timing(animatedOpacity, {
      toValue: toValue,
      duration: duration,
      useNativeDriver: true, //acelaracion por hardware
    }).start(callback);
  };

  const fadeOut = ({ duration = 300, toValue = 0, callback = () => { } }) => {
    Animated.timing(animatedOpacity, {
      toValue: toValue,
      duration: duration,
      useNativeDriver: true, //acelaracion por hardware
    }).start(callback);



  };

  const startMovingTopPosition = ({
    initialPosition = 0,
    toValue = 0,
    duration = 300,
    easing = Easing.linear,
    callback = () => { },
  }) => {
    animatedTop.setValue(initialPosition);
    Animated.timing(animatedTop, {
      toValue: toValue,
      duration: duration,
      useNativeDriver: true,
      easing: easing,

    }).start(callback);
  };

  return {
    //properties
    fadeIn,
    fadeOut,
    //methods
    animatedOpacity,
    animatedTop,
    startMovingTopPosition,
  };
};

Lo usamos..

import { Animated, Easing, Pressable, StyleSheet, Text, View } from 'react-native';
import React from 'react';
import { colors } from '../../../config/theme/theme';
import { useAnimation } from '../../hooks/useAnimation';

const Animation101Screen = () => {

  const { fadeOut, fadeIn, animatedTop, animatedOpacity, startMovingTopPosition } = useAnimation();

  return (
    <View style={styles.container}>
      <Animated.View style={[
        styles.purpleBox,
        {
          opacity: animatedOpacity,
          transform: [{ translateY: animatedTop }],
        },
      ]} />

      <Pressable onPress={() => {
        fadeIn({});
        startMovingTopPosition({
          initialPosition: -100,
          easing: Easing.elastic(1),
          duration: 700,
        });
      }} style={{ marginTop: 10 }}>

        <Text>FadeIN</Text>

      </Pressable>

      <Pressable onPress={() => fadeOut({})} style={{ marginTop: 10 }}>
        <Text>Fadeou</Text>
      </Pressable>
    </View >
  );
};

export default Animation101Screen;

const styles = StyleSheet.create({
  container: {
    justifyContent: 'center',
    flex: 1,
    alignItems: 'center',
  },
  purpleBox: {
    backgroundColor: colors.primary,
    width: 150,
    height: 150,
  },
});

Animated ValueXY

Creamos en le folder animations Animation102Screen

Intenta hacer este ejercicio que esta en la documentacion oficial

Solucion

import { StyleSheet, Animated, PanResponder } from 'react-native';
import React, { useRef } from 'react';
import { SafeAreaView, SafeAreaProvider } from 'react-native-safe-area-context';

const Animation102Screen = () => {
  const pan = useRef(new Animated.ValueXY()).current;

  const panResponder = PanResponder.create({
    onStartShouldSetPanResponder: () => true,
    onPanResponderMove: Animated.event([
      null,
      {
        dx: pan.x, // x,y are Animated.Value
        dy: pan.y,
      },
    ]),
    onPanResponderRelease: () => {
      Animated.spring(
        pan, // Auto-multiplexed
        { toValue: { x: 0, y: 0 }, useNativeDriver: false }, // Back to zero
      ).start();
    },
  });
  return (
    <SafeAreaProvider>
      <SafeAreaView style={styles.container}>
        <Animated.View
          {...panResponder.panHandlers}
          style={[pan.getLayout(), styles.box]}
        />
      </SafeAreaView>
    </SafeAreaProvider>
  );
};

export default Animation102Screen;


const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  box: {
    backgroundColor: '#61dafb',
    width: 80,
    height: 80,
    borderRadius: 4,
  },
});

Componentes personalizados

Crearemos estos componentes

ui/
├── Button.tsx
├── Card.tsx
└── CustomView.tsx

Creemos cada uno.

Button.tsx

import { Pressable, StyleProp, Text, ViewStyle } from 'react-native';
import React from 'react';
import { colors, globalStyles } from '../../config/theme/theme';
interface Props {
  text: string;
  styles?: StyleProp<ViewStyle>;
  onPress: () => void;
}
const Button = ({ text, styles, onPress }: Props) => {
  return (
    <Pressable
      onPress={onPress}
      style={({ pressed }) => ([
        styles,
        globalStyles.btnPrimary,
        {
          opacity: pressed ? 0.8 : 1,
          backgroundColor: colors.primary,
        },
      ])}
    >
      <Text style={[
        globalStyles.btnPrimaryText,
        {
          color: colors.buttonTextColor,
        },
      ]}>{text}</Text>
    </Pressable>
  );
};

export default Button;

Card.tsx

import { StyleProp, View, ViewStyle } from 'react-native';
import React, { PropsWithChildren } from 'react';
import { colors } from '../../config/theme/theme';

interface Props extends PropsWithChildren {
  style?: StyleProp<ViewStyle>;
}
const Card = ({ style, children }: Props) => {
  return (
    <View style={[
      {
        backgroundColor: colors.cardBackground,
        borderRadius: 10,
        padding: 10,
      },
      style,
    ]}>
      {children}
    </View>
  );
};

export default Card;

CustomView

import { StyleProp, View, ViewStyle } from 'react-native';
import React, { ReactNode } from 'react';
import { globalStyles } from '../../config/theme/theme';

interface Props {
  style?: StyleProp<ViewStyle>
  children?: ReactNode;
}
const CustomView = ({ children, style }: Props) => {
  return (
    <View style={[
      globalStyles.mainContainer,
      style,
    ]}>
      {children}
    </View>
  );
};

export default CustomView;

En el folder Switches creamos SwitchesScreen

import React from 'react';
import CustomView from '../ui/CustomView';

const SwitchScreen = () => {
  return (
    <CustomView style={{ paddingHorizontal: 10 }}>

     </CustomView>
  );
};

export default SwitchScreen;

Componente switch

documentacion oficial

Creamos en ui CustomSwitch

import { StyleSheet, Text, View } from 'react-native';
import React from 'react';
import { Switch } from 'react-native-gesture-handler';
import { colors } from '../../config/theme/theme';

interface Props {
  isOn: boolean;
  text?: string;

  onChange: (value: boolean) => void;
}

const CustomSwitch = ({ isOn, text, onChange }: Props) => {
  return (
    <View style={styles.switchRow}>
      {
        text && (<Text style={{ color: colors.text }}>{text}</Text>)
      }
      <Switch
        trackColor={{ false: '#767577', true: '#81b0ff' }}
        thumbColor={isOn ? '#f5dd4b' : '#f4f3f4'}
        ios_backgroundColor="#3e3e3e"
        onValueChange={onChange}
        value={isOn}
      />

    </View>
  );
};

export default CustomSwitch;

const styles = StyleSheet.create({
  switchRow: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    marginVertical: 5,
  },
});

En el folder Switches que tenemos creado SwitchesScreen

import React, { useState } from 'react';
import CustomView from '../ui/CustomView';
import Card from '../ui/Card';
import CustomSwitch from '../ui/CustomSwitch';

const SwitchScreen = () => {
  const [state, setState] = useState({
    isActive: true,
    isSad: false,
    isHappy: true,
  });

  return (
    <CustomView style={{ paddingHorizontal: 10 }}>
      <Card>
        <CustomSwitch
          isOn={state.isActive}
          onChange={value => setState(({ ...state, isActive: value }))}
          text="esta activo?"
        />
        <CustomSwitch
          isOn={state.isSad}
          onChange={value => setState(({ ...state, isSad: value }))}
          text="esta triste?"
        />

        <CustomSwitch
          isOn={state.isHappy}
          onChange={value => setState(({ ...state, isHappy: value }))}
          text="esta feliz?"
        />


      </Card>
    </CustomView>
  );
};

export default SwitchScreen;
  1. onChange={value => setState(({ ...state, isSad: value }))}

Pasamos value porque en CustomSwitch la prop onValueChange={onChange} cambia el valor que tenga actualmente en value={isOn} es por eso que no hacemos isSad:!isSad para invertir el valor.

Componente Alert

documentacion oficial

Vamos al folder alert y creamos AlertScreen

import { Alert } from 'react-native';
import React from 'react';
import CustomView from '../../ui/CustomView';
import { Title } from '../../ui/Title';
import { globalStyles } from '../../../config/theme/theme';
import Button from '../../ui/Button';
import Separator from '../../ui/Separator';

const AlerScreen = () => {
  const createTwoButtonAlert = () =>
    Alert.alert('Alert Title', 'My Alert Msg', [
      {
        text: 'Cancel',
        onPress: () => console.log('Cancel Pressed'),
        style: 'destructive',

      },
      { text: 'OK', onPress: () => console.log('OK Pressed') },
    ], {
      cancelable: true,
      onDismiss: () => {
        console.log('afuera');

      },
    });

  const createThreeButtonAlert = () =>
    Alert.alert('Alert Title', 'My Alert Msg', [
      {
        text: 'Ask me later',
        onPress: () => console.log('Ask me later pressed'),
      },
      {
        text: 'Cancel',
        onPress: () => console.log('Cancel Pressed'),
        style: 'cancel',
      },
      { text: 'OK', onPress: () => console.log('OK Pressed') },
    ]);

  return (
    <CustomView style={globalStyles.globalMargin}>
      <Title safe text="Alertas" />
      <Button
        onPress={createTwoButtonAlert}
        text="alerta - 2 botones" />
      <Separator />
      <Button
        onPress={createThreeButtonAlert}
        text="alerta - 3 botones" />
      <Separator />
      <Button
        onPress={() => { }}
        text="Promp input" />



    </CustomView>
  );
};

export default AlerScreen;
{
      cancelable: true,
      onDismiss: () => {
        console.log('afuera');

      },```

`cancelable` permite que cuando se haga click fuera del modal se cierre.
Pero en IOS no funciona , asi que podemos hacer uso de `onDismiss` 
para ejecutar la accion de cerrar.

Otra cosa que funciona en IOS pero no en android es `style: 'destructive'`
este hace que en IOS cancelar , se ponga en rojo, pero en Andriod no tiene 
efecto.

## Promp nativo

Para IOS funciona y para android no, en IOS seria :

```tsx 
  const showPromp = () => {
    Alert.prompt(
      'tu email?',
      'lorem ipson',
      (valor: string) => console.log(valor),
      'secure-text',
      'soy el valor por defecto',
      'number-pad'
    );
  };


 <Button onPress={showPromp} text="Promp input" />

Y eso es todo, ahora para tener una sola manera de manejar los inputs, vamos a usar un paquete.

documentacion oficial del paquete

  1. npm i react-native-prompt-android
import prompt from 'react-native-prompt-android';
prompt(
    'Enter password',
    'Enter your password to claim your $1.5B in lottery winnings',
    [
     {text: 'Cancel', onPress: () => console.log('Cancel Pressed'), style: 'cancel'},
     {text: 'OK', onPress: password => console.log('OK Pressed, password: ' + password)},
    ],
    {
        type: 'secure-text',
        cancelable: false,
        defaultValue: 'test',
        placeholder: 'placeholder'
    }
);

3. Reiniciamos el emulador

queda asi :

import { Alert } from 'react-native';
import React from 'react';
import CustomView from '../../ui/CustomView';
import { Title } from '../../ui/Title';
import { globalStyles } from '../../../config/theme/theme';
import Button from '../../ui/Button';
import Separator from '../../ui/Separator';

import prompt from 'react-native-prompt-android';
const AlerScreen = () => {
  const createTwoButtonAlert = () =>
    Alert.alert('Alert Title', 'My Alert Msg', [
      {
        text: 'Cancel',
        onPress: () => console.log('Cancel Pressed'),
        style: 'destructive',

      },
      { text: 'OK', onPress: () => console.log('OK Pressed') },
    ], {
      cancelable: true,
      onDismiss: () => {
        console.log('afuera');

      },
    });

  const createThreeButtonAlert = () =>
    Alert.alert('Alert Title', 'My Alert Msg', [
      {
        text: 'Ask me later',
        onPress: () => console.log('Ask me later pressed'),
      },
      {
        text: 'Cancel',
        onPress: () => console.log('Cancel Pressed'),
        style: 'cancel',
      },
      { text: 'OK', onPress: () => console.log('OK Pressed') },
    ]);


  const showPromp = () => {
    prompt(
      'Enter password',
      'Enter your password to claim your $1.5B in lottery winnings',
      [
        { text: 'Cancel', onPress: () => console.log('Cancel Pressed'), style: 'cancel' },
        { text: 'OK', onPress: password => console.log('OK Pressed, password: ' + password) },
      ],
      {
        type: 'secure-text',
        cancelable: false,
        defaultValue: 'test',
        placeholder: 'placeholder',
      }
    );
    //!nativo de IOS
    //   Alert.prompt(
    //     'tu email?',
    //     'lorem ipson',
    //     (valor: string) => console.log(valor),
    //     'secure-text',
    //     'soy el valor por defecto',
    //     'number-pad'
    //   );
  };

  return (
    <CustomView style={globalStyles.globalMargin}>
      <Title safe text="Alertas" />
      <Button
        onPress={createTwoButtonAlert}
        text="alerta - 2 botones" />
      <Separator />
      <Button
        onPress={createThreeButtonAlert}
        text="alerta - 3 botones" />
      <Separator />
      <Button
        onPress={showPromp}
        text="Promp input" />



    </CustomView>
  );
};

export default AlerScreen;

Adapter del paquete.

vamos a config/adapters/prop.adapter.ts

import prompt from 'react-native-prompt-android';

interface Buttons {
  text: string;
  onPress: (value: any) => void;
  style: 'cancel' | 'default' | 'destructive'
}

interface Options {
  textModal: string;
  descriptionModal: string;
  type: 'default' | 'plain-text' | 'secure-text'
  placeHolder: string;
  buttons: Buttons[]

}


export const showPrompAdapter = ({
  textModal,
  descriptionModal,
  type,
  placeHolder,
  buttons,
}: Options) => {
  prompt(
    textModal,
    descriptionModal,
    buttons,
    {
      type: type,
      cancelable: false,
      defaultValue: 'test',
      placeholder: placeHolder,
    }
  );

};

AlertScreen qued asi con el adaptador

import { Alert } from 'react-native';
import React from 'react';
import CustomView from '../../ui/CustomView';
import { Title } from '../../ui/Title';
import { globalStyles } from '../../../config/theme/theme';
import Button from '../../ui/Button';
import Separator from '../../ui/Separator';
import { showPrompAdapter } from '../../../config/adapters/prompt.adapter';

const AlerScreen = () => {
  const createTwoButtonAlert = () =>
    Alert.alert('Alert Title', 'My Alert Msg', [
      {
        text: 'Cancel',
        onPress: () => console.log('Cancel Pressed'),
        style: 'destructive',

      },
      { text: 'OK', onPress: () => console.log('OK Pressed') },
    ], {
      cancelable: true,
      onDismiss: () => {
        console.log('afuera');

      },
    });

  const createThreeButtonAlert = () =>
    Alert.alert('Alert Title', 'My Alert Msg', [
      {
        text: 'Ask me later',
        onPress: () => console.log('Ask me later pressed'),
      },
      {
        text: 'Cancel',
        onPress: () => console.log('Cancel Pressed'),
        style: 'cancel',
      },
      { text: 'OK', onPress: () => console.log('OK Pressed') },
    ]);


  const showPromp = () => {
    showPrompAdapter({
      textModal: 'la contra',
      descriptionModal: 'ponelo y listo',
      type: 'secure-text',
      placeHolder: 'pone algo',
      buttons: [
        { text: 'Cancel', onPress: () => console.log('Cancel Pressed'), style: 'cancel' },
        { text: 'ok', onPress: (value) => console.log(value), style: 'default' },
      ],

    });
    //!nativo de IOS
    //   Alert.prompt(
    //     'tu email?',
    //     'lorem ipson',
    //     (valor: string) => console.log(valor),
    //     'secure-text',
    //     'soy el valor por defecto',
    //     'number-pad'
    //   );
  };

  return (
    <CustomView style={globalStyles.globalMargin}>
      <Title safe text="Alertas" />
      <Button
        onPress={createTwoButtonAlert}
        text="alerta - 2 botones" />
      <Separator />
      <Button
        onPress={createThreeButtonAlert}
        text="alerta - 3 botones" />
      <Separator />
      <Button
        onPress={showPromp}
        text="Promp input" />



    </CustomView>
  );
};

export default AlerScreen;

Componente TextInput

documentacion oficial

Agregamos al theme.tsx

input: {
    height: 40,
    margin: 12,
    borderWidth: 1,
    padding: 10,
    borderColor: 'rgba(0,0,0,.3)',
    borderRadius: 10,
    color: colors.text,
  },

Creamos TextInputScreen en el folder input

import { Text, TextInput } from 'react-native';
import React, { useState } from 'react';
import CustomView from '../ui/CustomView';
import { Title } from '../ui/Title';
import { globalStyles } from '../../config/theme/theme';
import Card from '../ui/Card';
import Separator from '../ui/Separator';

const TextInputScreen = () => {

  const [form, setForm] = useState({
    name: '',
    email: '',
    phone: '',
  });

  return (
    <CustomView style={globalStyles.globalMargin}>
      <Title text="text inputs" safe />
      <Card>
        <TextInput
          style={globalStyles.input}
          placeholder="Nombre completo"
          autoCapitalize={'words'}
          autoCorrect={false}
          onChangeText={value => setForm({ ...form, name: value })}
        />
        <TextInput
          style={globalStyles.input}
          placeholder="email"
          autoCapitalize={'none'}
          autoCorrect={false}
          keyboardType="email-address"
          onChangeText={value => setForm({ ...form, email: value })}
        />
        <TextInput
          style={globalStyles.input}
          placeholder="phone"
          autoCapitalize={'none'}
          autoCorrect={false}
          keyboardType="phone-pad"
          onChangeText={value => setForm({ ...form, phone: value })}
        />


      </Card>

      <Separator />
      <Card>
        <Text>{JSON.stringify(form, null, 2)}</Text>

      </Card>
    </CustomView>
  );
};

export default TextInputScreen;

El tema es que ahora cuando el teclado se este mostrando para escribir no podemos hacer scroll.

vamos a solucionarlo.

Scroll y teclado

Para solucionarlo en android solo tenemos que envolver todo en ScrollView

<ScrollView>
      <CustomView style={globalStyles.globalMargin}>
        <Title text="text inputs" safe />
        <Card>
          <TextInput
            style={globalStyles.input}
            placeholder="Nombre completo"
            autoCapitalize={'words'}
            autoCorrect={false}
            onChangeText={value => setForm({ ...form, name: value })}
          />
          <TextInput
            style={globalStyles.input}
            placeholder="email"
            autoCapitalize={'none'}
            autoCorrect={false}
            keyboardType="email-address"
            onChangeText={value => setForm({ ...form, email: value })}
          />
          <TextInput
            style={globalStyles.input}
            placeholder="phone"
            autoCapitalize={'none'}
            autoCorrect={false}
            keyboardType="phone-pad"
            onChangeText={value => setForm({ ...form, phone: value })}
          />


        </Card>

        <Separator />
        <Card>
          <Text>{JSON.stringify(form, null, 2)}</Text>
          <Text>{JSON.stringify(form, null, 2)}</Text>
          <Text>{JSON.stringify(form, null, 2)}</Text>
          <Text>{JSON.stringify(form, null, 2)}</Text>
          <Text>{JSON.stringify(form, null, 2)}</Text>
          <Text>{JSON.stringify(form, null, 2)}</Text>
          <Text>{JSON.stringify(form, null, 2)}</Text>
          <Text>{JSON.stringify(form, null, 2)}</Text>
          <Text>{JSON.stringify(form, null, 2)}</Text>

        </Card>
      </CustomView>

    </ScrollView>

Pero para que funcione en IOS tenemos que envolverlo en keyboardAvoidingView como lo muestra la documentacion oficial

<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : undefined}>

      <ScrollView>
        <CustomView style={globalStyles.globalMargin}>
          <Title text="text inputs" safe />
          <Card>
            <TextInput
              style={globalStyles.input}
              placeholder="Nombre completo"
              autoCapitalize={'words'}
              autoCorrect={false}
              onChangeText={value => setForm({ ...form, name: value })}
            />
            <TextInput
              style={globalStyles.input}
              placeholder="email"
              autoCapitalize={'none'}
              autoCorrect={false}
              keyboardType="email-address"
              onChangeText={value => setForm({ ...form, email: value })}
            />
            <TextInput
              style={globalStyles.input}
              placeholder="phone"
              autoCapitalize={'none'}
              autoCorrect={false}
              keyboardType="phone-pad"
              onChangeText={value => setForm({ ...form, phone: value })}
            />


          </Card>

          <Separator />
          <Card>
            <Text>{JSON.stringify(form, null, 2)}</Text>
            <Text>{JSON.stringify(form, null, 2)}</Text>
            <Text>{JSON.stringify(form, null, 2)}</Text>
            <Text>{JSON.stringify(form, null, 2)}</Text>
            <Text>{JSON.stringify(form, null, 2)}</Text>
            <Text>{JSON.stringify(form, null, 2)}</Text>
            <Text>{JSON.stringify(form, null, 2)}</Text>
            <Text>{JSON.stringify(form, null, 2)}</Text>
            <Text>{JSON.stringify(form, null, 2)}</Text>

          </Card>
        </CustomView>

      </ScrollView>

    </KeyboardAvoidingView>

Es importante poner el udefined en Platform.OS === 'ios' ? 'padding' : undefined asi no arruina el comportamiento en Android.

Pull to refresh

documentacion oficial

Creamos el componente en ui/PullToRefreshScreen y lo agregamos como ruta del StackNav

import { RefreshControl, ScrollView, Text } from 'react-native';
import React from 'react';
import { Title } from './Title';
import CustomView from './CustomView';

const PullToRefreshScreen = () => {
  return (
    <ScrollView
      refreshControl={<RefreshControl
        refreshing={true}
      />}
    >
      <CustomView margin>
        <Title text="pull to refresh" safe />
        <Text>PullToRefreshScreen</Text>
      </CustomView>
    </ScrollView>
  );
};

export default PullToRefreshScreen;

Hasta ahi veremos el load, pero en ios por el notch no veremos el loading, asi que lo solucionamos de la siguiente manera.

import { RefreshControl, ScrollView, Text } from 'react-native';
import React from 'react';
import { Title } from './Title';
import CustomView from './CustomView';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

const PullToRefreshScreen = () => {
  const { top } = useSafeAreaInsets();
  return (
    <ScrollView
      refreshControl={
        <RefreshControl
          refreshing={true}
          progressViewOffset={top}
        />}
    >
      <CustomView margin>
        <Title text="pull to refresh" safe />
        <Text>PullToRefreshScreen</Text>
      </CustomView>
    </ScrollView>
  );
};

export default PullToRefreshScreen;

La implementacion final seria :

import { RefreshControl, ScrollView, Text } from 'react-native';
import React, { useState } from 'react';
import { Title } from './Title';
import CustomView from './CustomView';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { colors, globalStyles } from '../../config/theme/theme';

const PullToRefreshScreen = () => {
  const [isRefreshing, setIsRefreshing] = useState(false);

  const { top } = useSafeAreaInsets();

  const onRefresh = () => {
    setIsRefreshing(true);
    setTimeout(() => {

      setIsRefreshing(false);
    }, 2000);
  };

  return (
    <ScrollView
      refreshControl={
        <RefreshControl
          refreshing={isRefreshing}
          progressViewOffset={top}
          colors={[colors.primary, 'red', 'orange']}
          onRefresh={onRefresh}
        />}

      style={[globalStyles.mainContainer, globalStyles.globalMargin]}
    >
      <CustomView margin>
        <Title text="pull to refresh" safe />
        <Text>PullToRefreshScreen</Text>
      </CustomView>
    </ScrollView>
  );
};

export default PullToRefreshScreen;

Componente - sectionList

documentacion oficial

Estos son los datos que usaremos para probar este Componente :

interface Houses {
  title: string;
  data: string[];
}

const houses: Houses[] = [
  {
    title: 'DC Comics',
    data: [
      'Superman',
      'Batman',
      'Wonder Woman (Mujer Maravilla)',
      'The Flash (Flash)',
      'Aquaman',
      'Green Lantern (Linterna Verde)',
      'Cyborg',
      'Shazam',
      'Green Arrow (Flecha Verde)',
      'Batgirl (Batichica)',
      'Nightwing (Ala Nocturna)',
      'Supergirl',
      'Martian Manhunter (Detective Marciano)',
      'Harley Quinn',
      'Joker',
      'Catwoman (Gata Salvaje)',
      'Lex Luthor',
      'Poison Ivy (Hiedra Venenosa)',
      'Robin',
      'Deathstroke (Deathstroke el Terminator)',
    ],
  },
  {
    title: 'Marvel Comics',
    data: [
      'Spider-Man (Hombre Araña)',
      'Iron Man (Hombre de Hierro)',
      'Captain America (Capitán América)',
      'Thor',
      'Black Widow (Viuda Negra)',
      'Hulk',
      'Doctor Strange (Doctor Extraño)',
      'Black Panther (Pantera Negra)',
      'Captain Marvel (Capitana Marvel)',
      'Wolverine',
      'Deadpool',
      'Scarlet Witch (Bruja Escarlata)',
      'Ant-Man (Hombre Hormiga)',
      'Wasp (Avispa)',
      'Groot',
      'Rocket Raccoon (Mapache Cohete)',
      'Gamora',
      'Drax the Destroyer (Drax el Destructor)',
    ],
  },
  {
    title: 'Anime',
    data: [
      'Son Goku (Dragon Ball)',
      'Naruto Uzumaki (Naruto)',
      'Monkey D. Luffy (One Piece)',
      'Sailor Moon (Sailor Moon)',
      'Kenshin Himura (Rurouni Kenshin)',
      'Edward Elric (Fullmetal Alchemist)',
      'Inuyasha (Inuyasha)',
      'Sakura Kinomoto (Cardcaptor Sakura)',
      'Light Yagami (Death Note)',
      'Eren Yeager (Attack on Titan)',
      'Lelouch Lamperouge (Code Geass)',
      'Vegeta (Dragon Ball)',
      'Ichigo Kurosaki (Bleach)',
      'Kaneki Ken (Tokyo Ghoul)',
      'Gon Freecss (Hunter x Hunter)',
      'Asuka Langley Soryu (Neon Genesis Evangelion)',
      'Saitama (One Punch Man)',
      'Mikasa Ackerman (Attack on Titan)',
      'Natsu Dragneel (Fairy Tail)',
      'Usagi Tsukino (Sailor Moon)',
      'Sasuke Uchiha (Naruto)',
    ],
  },
];

Creamos un ui el componente CustomSectionListScreen: y agrega a StackNav la ruta del nuevo componente.

import { SectionList, Text, useWindowDimensions } from 'react-native';
import React from 'react';
import CustomView from './CustomView';
import { Title } from './Title';
import Card from './Card';
import Separator from './Separator';
import { useSafeAreaInsets } from 'react-native-safe-area-context';


interface Houses {
  title: string;
  data: string[];
}

const houses: Houses[] = [
  {
    title: 'DC Comics',
    data: [
      'Superman',
      'Batman',
      'Wonder Woman (Mujer Maravilla)',
      'The Flash (Flash)',
      'Aquaman',
      'Green Lantern (Linterna Verde)',
      'Cyborg',
      'Shazam',
      'Green Arrow (Flecha Verde)',
      'Batgirl (Batichica)',
      'Nightwing (Ala Nocturna)',
      'Supergirl',
      'Martian Manhunter (Detective Marciano)',
      'Harley Quinn',
      'Joker',
      'Catwoman (Gata Salvaje)',
      'Lex Luthor',
      'Poison Ivy (Hiedra Venenosa)',
      'Robin',
      'Deathstroke (Deathstroke el Terminator)',
    ],
  },
  {
    title: 'Marvel Comics',
    data: [
      'Spider-Man (Hombre Araña)',
      'Iron Man (Hombre de Hierro)',
      'Captain America (Capitán América)',
      'Thor',
      'Black Widow (Viuda Negra)',
      'Hulk',
      'Doctor Strange (Doctor Extraño)',
      'Black Panther (Pantera Negra)',
      'Captain Marvel (Capitana Marvel)',
      'Wolverine',
      'Deadpool',
      'Scarlet Witch (Bruja Escarlata)',
      'Ant-Man (Hombre Hormiga)',
      'Wasp (Avispa)',
      'Groot',
      'Rocket Raccoon (Mapache Cohete)',
      'Gamora',
      'Drax the Destroyer (Drax el Destructor)',
    ],
  },
  {
    title: 'Anime',
    data: [
      'Son Goku (Dragon Ball)',
      'Naruto Uzumaki (Naruto)',
      'Monkey D. Luffy (One Piece)',
      'Sailor Moon (Sailor Moon)',
      'Kenshin Himura (Rurouni Kenshin)',
      'Edward Elric (Fullmetal Alchemist)',
      'Inuyasha (Inuyasha)',
      'Sakura Kinomoto (Cardcaptor Sakura)',
      'Light Yagami (Death Note)',
      'Eren Yeager (Attack on Titan)',
      'Lelouch Lamperouge (Code Geass)',
      'Vegeta (Dragon Ball)',
      'Ichigo Kurosaki (Bleach)',
      'Kaneki Ken (Tokyo Ghoul)',
      'Gon Freecss (Hunter x Hunter)',
      'Asuka Langley Soryu (Neon Genesis Evangelion)',
      'Saitama (One Punch Man)',
      'Mikasa Ackerman (Attack on Titan)',
      'Natsu Dragneel (Fairy Tail)',
      'Usagi Tsukino (Sailor Moon)',
      'Sasuke Uchiha (Naruto)',
    ],
  },
];
const CustomSectionListScreen = () => {
  const { height } = useWindowDimensions();
  const { top } = useSafeAreaInsets();
  return (
    <CustomView margin>
      <Title text="lista de personajes" />
      <Card>
        <SectionList sections={houses}
          keyExtractor={(item) => item}
          renderItem={({ item }) => <Text>{item}</Text>}
          renderSectionHeader={({ section }) => <Title text={section.title} />}
          stickySectionHeadersEnabled
          ListHeaderComponent={() => <Title text="Personajes" />}
          SectionSeparatorComponent={Separator}
          showsVerticalScrollIndicator={false}
          ListFooterComponent={() => <Title text={`SECCIONES : ${houses.length}`} />}
          style={{
            height: height - top - 150,
          }}
        />
      </Card>
    </CustomView>

  );
};

export default CustomSectionListScreen;

ModalScreen

documentacion oficial

ui/ModalScreen.tsx

import { Modal, Platform, View } from 'react-native';
import React, { useState } from 'react';
import CustomView from './CustomView';
import { Title } from './Title';
import Button from './Button';

const ModalScreen = () => {

  const [isVisible, setIsVisible] = useState(false);
  return (
    <CustomView margin>
      <Title text="Modal" safe />
      <Button
        text="Abrir modal"
        onPress={() => setIsVisible(true)}
      />

      <Modal visible={isVisible} animationType="slide">
        <View style={{
          flex: 1,
          backgroundColor: 'rgba(0,0,0,.1)',
        }}>
          <View style={{ paddingHorizontal: 10 }}>
            <Title text="modal content" safe />
          </View>
          <View style={{ flex: 1 }} />
          <Button
            onPress={() => setIsVisible(false)}
            text="cerrar"
            styles={{
              height: Platform.OS === 'android' ? 40 : 60,
              borderRadius: 0,
            }}
          />
        </View>
      </Modal>
    </CustomView>
  );
};

export default ModalScreen;

Infinite Scroll

Lo haremos con lorem picsum

ui/InfiniteScrollScreen y lo agregamos a StackNav

import { useState } from 'react';
import { ActivityIndicator, View } from 'react-native';
import { FlatList } from 'react-native-gesture-handler';
import { colors } from '../../config/theme/theme';
import { FadeInImage } from './FadeInImage';

export const InfiniteScrollScreen = () => {
  const [numbers, setNumbers] = useState([0, 1, 2, 3, 4, 5]);

  const loadMore = () => {
    const newArray = Array.from({ length: 5 }, (_, i) => numbers.length + i);

    setTimeout(() => {
      setNumbers([...numbers, ...newArray]);
    }, 3000);
  };

  return (
    <View style={{ backgroundColor: 'black' }}>
      <FlatList
        data={numbers}
        onEndReached={loadMore}
        onEndReachedThreshold={0.6}
        keyExtractor={item => item.toString()}
        renderItem={({ item }) => <ListItem number={item} />}
        ListFooterComponent={() => (
          <View style={{ height: 150, justifyContent: 'center' }}>
            <ActivityIndicator size={40} color={colors.primary} />
          </View>
        )}
      />
    </View>
  );
};

interface ListItemProps {
  number: number;
}

const ListItem = ({ number }: ListItemProps) => {
  return (
    <FadeInImage
      uri={`https://picsum.photos/id/${number}/500/400`}
      style={{
        height: 400,
        width: '100%',
      }}
    />

  );
};

1. Importaciones

import { useState } from 'react';
import { ActivityIndicator, View } from 'react-native';
import { FlatList } from 'react-native-gesture-handler';
import { colors } from '../../config/theme/theme';
import { FadeInImage } from './FadeInImage';
  • useState: Hook para manejar el estado en componentes funcionales.
  • ActivityIndicator: Componente que muestra un indicador de carga animado.
  • View: Contenedor básico en React Native.
  • FlatList: Componente optimizado para manejar listas grandes.
  • colors: Un archivo de configuración que contiene el tema de la aplicación (en este caso, colors.primary).
  • FadeInImage: Componente personalizado para mostrar imágenes con efecto de aparición gradual.

2. Estado inicial y función para cargar más elementos

const [numbers, setNumbers] = useState([0, 1, 2, 3, 4, 5]);

const loadMore = () => {
  const newArray = Array.from({ length: 5 }, (_, i) => numbers.length + i);

  setTimeout(() => {
    setNumbers([...numbers, ...newArray]);
  }, 3000);
};
  • numbers: Estado que contiene un arreglo de números que se usa para generar las URLs de las imágenes.
  • setNumbers: Función para actualizar el estado.
  • loadMore: Función que añade más números al arreglo existente:
    • Genera un nuevo arreglo con 5 números consecutivos.
    • Simula un retraso de 3 segundos usando setTimeout antes de actualizar el estado.

3. Renderizado del componente principal

return (
  <View style={{ backgroundColor: 'black' }}>
    <FlatList
      data={numbers}
      onEndReached={loadMore}
      onEndReachedThreshold={0.6}
      keyExtractor={item => item.toString()}
      renderItem={({ item }) => <ListItem number={item} />}
      ListFooterComponent={() => (
        <View style={{ height: 150, justifyContent: 'center' }}>
          <ActivityIndicator size={40} color={colors.primary} />
        </View>
      )}
    />
  </View>
);
  1. View:

    • Contenedor con fondo negro.
  2. FlatList:

    • data: Datos que se mostrarán (el estado numbers).
    • onEndReached: Función que se ejecuta al llegar al final de la lista (loadMore).
    • onEndReachedThreshold: Define el umbral de activación de onEndReached (0.6 significa que se activará cuando queden 60% de elementos visibles antes del final).
    • keyExtractor: Genera una clave única para cada elemento (en este caso, el número convertido a string).
    • renderItem: Función que renderiza cada elemento (usa el componente ListItem).
    • ListFooterComponent: Componente mostrado al final de la lista (un indicador de carga).

4. Componente ListItem

const ListItem = ({ number }: ListItemProps) => {
  return (
    <FadeInImage
      uri={`https://picsum.photos/id/${number}/500/400`}
      style={{
        height: 400,
        width: '100%',
      }}
    />
  );
};
  • Props:
    • number: Número recibido como prop.
  • Contenido:
    • Renderiza un componente FadeInImage con una URL basada en number.
    • La imagen es obtenida de https://picsum.photos, un servicio de imágenes aleatorias.

5. Lógica de FadeInImage

Aunque el código de FadeInImage no está incluido, probablemente sea un componente personalizado que muestra imágenes con un efecto de “fade-in” (aparición gradual).


6. Flujo completo

  1. Se inicializa el estado con números [0, 1, 2, 3, 4, 5].
  2. FlatList muestra estos números como imágenes, generando la URL con number.
  3. Al acercarse al final de la lista (onEndReached), se ejecuta loadMore:
    • Se agregan 5 números más al estado después de 3 segundos.
  4. Durante la carga, se muestra un ActivityIndicator al final de la lista.
  5. El proceso se repite indefinidamente.

Detalles importantes

  • Eficiencia: FlatList está optimizado para manejar listas grandes, renderizando solo los elementos visibles y algunos elementos adicionales (buffer).
  • Experiencia del usuario: ActivityIndicator indica al usuario que se están cargando más elementos.
  • Adaptabilidad: El diseño utiliza estilos responsivos (height y width) para adaptarse a diferentes dispositivos.

FadeInImage

Creamos el componetne FadeInImage

import { useState } from 'react';
import {
  ActivityIndicator,
  Animated,
  ImageStyle,
  StyleProp,
  View,
} from 'react-native';
import { useAnimation } from '../hooks/useAnimation';

interface Props {
  uri: string;
  style?: StyleProp<ImageStyle>;
}

export const FadeInImage = ({ uri, style }: Props) => {
  const { animatedOpacity, fadeIn } = useAnimation();
  const [isLoading, setIsLoading] = useState(true);

  return (
    <View style={{ justifyContent: 'center', alignItems: 'center' }}>
      {isLoading && (
        <ActivityIndicator
          style={{ position: 'absolute' }}
          color="grey"
          size={30}
        />
      )}

      <Animated.Image
        source={{ uri }}
        onLoadEnd={() => {
          fadeIn({});
          setIsLoading(false);
        }}
        style={[style, { opacity: animatedOpacity }]}
      />
    </View>
  );
};

Este código implementa un componente llamado FadeInImage, que muestra una imagen con un efecto de aparición gradual (fade-in) utilizando animaciones.


1. Importaciones

import { useState } from 'react';
import {
  ActivityIndicator,
  Animated,
  ImageStyle,
  StyleProp,
  View,
} from 'react-native';
import { useAnimation } from '../hooks/useAnimation';
  • useState: Hook para manejar el estado del componente.
  • ActivityIndicator: Componente que muestra un indicador de carga.
  • Animated: Biblioteca para crear animaciones en React Native.
  • StyleProp y ImageStyle: Tipos para los estilos, usados para tipar correctamente las props.
  • useAnimation: Hook personalizado para manejar la animación de opacidad.

2. Props del componente

interface Props {
  uri: string; // URL de la imagen.
  style?: StyleProp<ImageStyle>; // Estilos opcionales para la imagen.
}
  • uri: URL de la imagen que se va a cargar.
  • style: Prop opcional que permite personalizar el estilo de la imagen.

3. Estados y animación

const { animatedOpacity, fadeIn } = useAnimation();
const [isLoading, setIsLoading] = useState(true);
  1. animatedOpacity y fadeIn:

    • animatedOpacity: Valor animado que controla la opacidad de la imagen.
    • fadeIn: Función que inicia la animación para cambiar gradualmente la opacidad.

    Estos valores provienen del hook useAnimation, que está diseñado para manejar animaciones (probablemente inicializa un valor animado y proporciona funciones para controlarlo).

  2. isLoading:

    • Estado que indica si la imagen está en proceso de carga (true) o si ya se ha cargado (false).
    • Se usa para mostrar u ocultar el ActivityIndicator.

4. Renderizado del componente

return (
  <View style={{ justifyContent: 'center', alignItems: 'center' }}>
    {isLoading && (
      <ActivityIndicator
        style={{ position: 'absolute' }}
        color="grey"
        size={30}
      />
    )}

    <Animated.Image
      source={{ uri }}
      onLoadEnd={() => {
        fadeIn({});
        setIsLoading(false);
      }}
      style={[style, { opacity: animatedOpacity }]}
    />
  </View>
);
  1. Contenedor principal (View):

    • Centra vertical y horizontalmente el contenido.
  2. Indicador de carga (ActivityIndicator):

    • Muestra un spinner mientras la imagen se está cargando.
    • Está posicionado en el centro mediante position: 'absolute'.
  3. Imagen animada (Animated.Image):

    • source={{ uri }}: La imagen se carga desde la URL proporcionada en las props.
    • onLoadEnd:
      • Se ejecuta cuando la imagen ha terminado de cargarse.
      • Llama a fadeIn({}) para iniciar la animación de opacidad.
      • Cambia isLoading a false para ocultar el indicador de carga.
    • style:
      • Combina el estilo recibido en las props con el estilo animado de opacidad ({ opacity: animatedOpacity }).

5. Flujo del componente

  1. Cuando el componente se monta, el estado isLoading está en true y se muestra el ActivityIndicator.
  2. La imagen comienza a cargarse desde la URL proporcionada.
  3. Una vez que la imagen termina de cargar (onLoadEnd):
    • Se oculta el ActivityIndicator (setIsLoading(false)).
    • Se inicia la animación de opacidad (fadeIn).
  4. La imagen aparece gradualmente.

Ventajas

  • Experiencia de usuario mejorada: El efecto de aparición gradual hace que la transición sea más agradable visualmente.
  • Carga progresiva: El ActivityIndicator informa al usuario que algo se está cargando.
  • Reutilizable: Este componente puede usarse en cualquier lugar donde se necesite mostrar imágenes con un efecto fade-in.

Slideshow

Podemos buscar paquetes como snap carousel pero lo haremos vanilla.

La data que usaremos es esta :

interface Slide {
  title: string;
  desc: string;
  img: ImageSourcePropType;
}

const items: Slide[] = [
  {
    title: 'Titulo 1',
    desc: 'Ea et eu enim fugiat sunt reprehenderit sunt aute quis tempor ipsum cupidatat et.',
    img: require('../../assets/slide-1.png'),
  },
  {
    title: 'Titulo 2',
    desc: 'Anim est quis elit proident magna quis cupidatat curlpa labore Lorem ea. Exercitation mollit velit in aliquip tempor occaecat dolor minim amet dolor enim cillum excepteur. ',
    img: require('../../assets/slide-2.png'),
  },
  {
    title: 'Titulo 3',
    desc: 'Ex amet duis amet nulla. Aliquip ea Lorem ea culpa consequat proident. Nulla tempor esse ad tempor sit amet Lorem. Velit ea labore aute pariatur commodo duis veniam enim.',
    img: require('../../assets/slide-3.png'),
  },
];

Las imagenes las sacamos de undraw

SlidesScreen queda asi :

import { ImageSourcePropType, StyleSheet, Text, View } from 'react-native';
import React from 'react';

interface Slide {
  title: string;
  desc: string;
  img: ImageSourcePropType;
}

const items: Slide[] = [
  {
    title: 'Titulo 1',
    desc: 'Ea et eu enim fugiat sunt reprehenderit sunt aute quis tempor ipsum cupidatat et.',
    img: require('../../assets/slide-1.png'),
  },
  {
    title: 'Titulo 2',
    desc: 'Anim est quis elit proident magna quis cupidatat curlpa labore Lorem ea. Exercitation mollit velit in aliquip tempor occaecat dolor minim amet dolor enim cillum excepteur. ',
    img: require('../../assets/slide-2.png'),
  },
  {
    title: 'Titulo 3',
    desc: 'Ex amet duis amet nulla. Aliquip ea Lorem ea culpa consequat proident. Nulla tempor esse ad tempor sit amet Lorem. Velit ea labore aute pariatur commodo duis veniam enim.',
    img: require('../../assets/slide-3.png'),
  },
];

const SlideScreen = () => {
  return (
    <View>
      <Text>SlideScreen</Text>
    </View>
  );
};

export default SlideScreen;

const styles = StyleSheet.create({});

En este punto tendremos un erro porque las imagenes en assets nos existe, vamos a crearlo en presentation/assets/ y ponemos 3 imagenes con el Nombre que estan en el objeto items.

import { Image, ImageSourcePropType, NativeScrollEvent, NativeSyntheticEvent, Text, View, useWindowDimensions } from 'react-native';
import React, { useRef, useState } from 'react';
import { colors, globalStyles } from '../../config/theme/theme';
import { FlatList } from 'react-native-gesture-handler';
import Button from './Button';
import { useNavigation } from '@react-navigation/native';

interface Slide {
  title: string;
  desc: string;
  img: ImageSourcePropType;
}

const items: Slide[] = [
  {
    title: 'Titulo 1',
    desc: 'Ea et eu enim fugiat sunt reprehenderit sunt aute quis tempor ipsum cupidatat et.',
    img: require('../assets/slide-1.png'),
  },
  {
    title: 'Titulo 2',
    desc: 'Anim est quis elit proident magna quis cupidatat curlpa labore Lorem ea. Exercitation mollit velit in aliquip tempor occaecat dolor minim amet dolor enim cillum excepteur. ',
    img: require('../assets/slide-2.png'),
  },
  {
    title: 'Titulo 3',
    desc: 'Ex amet duis amet nulla. Aliquip ea Lorem ea culpa consequat proident. Nulla tempor esse ad tempor sit amet Lorem. Velit ea labore aute pariatur commodo duis veniam enim.',
    img: require('../assets/slide-3.png'),
  },
];

const SlideScreen = () => {
  const [currentSlideIndex, setCurrentSlideIndex] = useState(0);
  const flatListRef = useRef<FlatList>(null);
  const navigation = useNavigation();
  const onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
    const { contentOffset, layoutMeasurement } = event.nativeEvent;
    const currentIndex = Math.floor(contentOffset.x / layoutMeasurement.width);
    setCurrentSlideIndex(currentIndex > 0 ? currentIndex : 0);
  };


  const scrollToSlide = (index: number) => {
    if (!flatListRef.current) { return; }

    flatListRef.current.scrollToIndex({ index: index, animated: true });
  };


  return (
    <View style={{
      flex: 1,
      backgroundColor: colors.background,
    }}>
      <FlatList
        ref={flatListRef}
        data={items}
        keyExtractor={(item) => item.title}
        renderItem={({ item }) => <SlideItem item={item} />}
        horizontal
        pagingEnabled
        scrollEnabled={false}
        onScroll={onScroll}

      />
      {
        currentSlideIndex == items.length - 1 ? (
          <Button
            text="finalizar"
            onPress={() => navigation.goBack()}
          />
        ) :
          <Button
            styles={{ position: 'absolute', bottom: 50, right: 30 }}
            text="siguiente"
            onPress={() => scrollToSlide(currentSlideIndex + 1)}
          />

      }
    </View>
  );
};

export default SlideScreen;

interface SlideItemProps {
  item: Slide;
}
const SlideItem = ({ item }: SlideItemProps) => {
  const { width } = useWindowDimensions();
  const { title, desc, img } = item;
  return (
    <View
      style={{ flex: 1, backgroundColor: 'white', borderRadius: 5, padding: 40, justifyContent: 'center', width: width }}
    >
      <Image
        source={img}
        style={{
          width: width * 0.7,
          height: width * 0.7,
          resizeMode: 'center',
          alignSelf: 'center',
        }}
      />
      <Text
        style={[
          globalStyles.title,
          { color: colors.primary },
        ]}
      >
        {title}</Text>

      <Text
        style={{
          color: colors.text,
          marginTop: 20,
        }}
      >
        {desc}</Text>

    </View>

  );

};

Este código implementa una pantalla de slides horizontal (o tutorial interactivo) con la posibilidad de avanzar al siguiente slide usando un botón. Aquí tienes una explicación detallada de cada parte del código:


1. Importaciones

import { Image, ImageSourcePropType, NativeScrollEvent, NativeSyntheticEvent, Text, View, useWindowDimensions } from 'react-native';
import React, { useRef, useState } from 'react';
import { colors, globalStyles } from '../../config/theme/theme';
import { FlatList } from 'react-native-gesture-handler';
import Button from './Button';
import { useNavigation } from '@react-navigation/native';
  • useState y useRef: Hooks para manejar el estado del slide actual y referenciar la FlatList respectivamente.
  • useNavigation: Hook para manejar la navegación entre pantallas.
  • FlatList: Componente optimizado para listas grandes, aquí se usa para renderizar los slides de manera horizontal.
  • useWindowDimensions: Hook que obtiene las dimensiones de la ventana del dispositivo (útil para calcular el ancho de los slides).

2. Estructura de los datos

interface Slide {
  title: string;
  desc: string;
  img: ImageSourcePropType;
}

const items: Slide[] = [
  {
    title: 'Titulo 1',
    desc: 'Ea et eu enim fugiat sunt reprehenderit sunt aute quis tempor ipsum cupidatat et.',
    img: require('../assets/slide-1.png'),
  },
  // Otros slides...
];
  • Slide: Define la estructura del objeto para cada slide (title, desc, img).
  • items: Arreglo que contiene los datos de cada slide.

3. Estado y referencias

const [currentSlideIndex, setCurrentSlideIndex] = useState(0);
const flatListRef = useRef<FlatList>(null);
const navigation = useNavigation();
  • currentSlideIndex: Guarda el índice del slide actual.
  • flatListRef: Referencia al componente FlatList para interactuar programáticamente (e.g., desplazarse entre slides).
  • navigation: Se utiliza para volver a la pantalla anterior cuando el usuario finaliza los slides.

4. Manejo del desplazamiento

Detectar el slide actual

const onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
  const { contentOffset, layoutMeasurement } = event.nativeEvent;
  const currentIndex = Math.floor(contentOffset.x / layoutMeasurement.width);
  setCurrentSlideIndex(currentIndex > 0 ? currentIndex : 0);
};
  • onScroll: Se ejecuta cuando el usuario se desplaza horizontalmente.
    • contentOffset.x: Indica cuánto se ha desplazado horizontalmente.
    • layoutMeasurement.width: Ancho de la vista visible.
    • Calcula el índice del slide actual dividiendo contentOffset.x por el ancho visible (layoutMeasurement.width).

Desplazar al siguiente slide

const scrollToSlide = (index: number) => {
  if (!flatListRef.current) return;
  flatListRef.current.scrollToIndex({ index: index, animated: true });
};
  • scrollToSlide: Utiliza la referencia de FlatList para desplazarse al slide indicado.
  • scrollToIndex: Método de FlatList que permite desplazarse al índice deseado.

5. Renderizado principal

<View style={{ flex: 1, backgroundColor: colors.background }}>
  <FlatList
    ref={flatListRef}
    data={items}
    keyExtractor={(item) => item.title}
    renderItem={({ item }) => <SlideItem item={item} />}
    horizontal
    pagingEnabled
    scrollEnabled={false}
    onScroll={onScroll}
  />
  {currentSlideIndex == items.length - 1 ? (
    <Button text="finalizar" onPress={() => navigation.goBack()} />
  ) : (
    <Button
      styles={{ position: 'absolute', bottom: 50, right: 30 }}
      text="siguiente"
      onPress={() => scrollToSlide(currentSlideIndex + 1)}
    />
  )}
</View>

FlatList

  • ref={flatListRef}: Asocia la referencia para interactuar con el componente.
  • data={items}: Proporciona los datos de los slides.
  • horizontal y pagingEnabled: Configura el desplazamiento horizontal con paginación.
  • scrollEnabled={false}: Desactiva el desplazamiento manual.
  • onScroll: Detecta el slide actual cuando ocurre un desplazamiento.

Botones

  • Botón “siguiente”:
    • Aparece mientras no esté en el último slide.
    • Llama a scrollToSlide para moverse al siguiente slide.
  • Botón “finalizar”:
    • Aparece en el último slide.
    • Llama a navigation.goBack() para volver a la pantalla anterior.

6. Componente SlideItem

const SlideItem = ({ item }: SlideItemProps) => {
  const { width } = useWindowDimensions();
  const { title, desc, img } = item;

  return (
    <View
      style={{
        flex: 1,
        backgroundColor: 'white',
        borderRadius: 5,
        padding: 40,
        justifyContent: 'center',
        width: width,
      }}
    >
      <Image
        source={img}
        style={{
          width: width * 0.7,
          height: width * 0.7,
          resizeMode: 'center',
          alignSelf: 'center',
        }}
      />
      <Text style={[globalStyles.title, { color: colors.primary }]}>{title}</Text>
      <Text style={{ color: colors.text, marginTop: 20 }}>{desc}</Text>
    </View>
  );
};
  • SlideItem:
    • Recibe los datos del slide (item) y renderiza el contenido (imagen, título y descripción).
    • Calcula dinámicamente el ancho del contenedor usando useWindowDimensions.

7. Flujo de la funcionalidad

  1. La pantalla muestra el primer slide.
  2. El botón “siguiente” avanza al próximo slide.
  3. En el último slide, el botón cambia a “finalizar”.
  4. Al pulsar “finalizar”, vuelve a la pantalla anterior.

Ventajas

  • Modularidad: Los slides (SlideItem) y la lógica del componente principal (SlideScreen) están bien separados.
  • Efecto de desplazamiento controlado: FlatList maneja el desplazamiento horizontal y los botones controlan el cambio de slides.
  • Responsividad: Usa useWindowDimensions para adaptarse a diferentes tamaños de pantalla.