Setup proyecto
Instalacion
npx @react-native-community/cli@latest init Components
- Intalamos StackNav que lo vimos en este post pre instalacion y aca la instlacion en si
Como ya sos grande te dejo la documentacion
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>
);
};
Menu Item
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
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
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;
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
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
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
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
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
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
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>
);
-
View
:- Contenedor con fondo negro.
-
FlatList
:data
: Datos que se mostrarán (el estadonumbers
).onEndReached
: Función que se ejecuta al llegar al final de la lista (loadMore
).onEndReachedThreshold
: Define el umbral de activación deonEndReached
(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 componenteListItem
).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 ennumber
. - La imagen es obtenida de
https://picsum.photos
, un servicio de imágenes aleatorias.
- Renderiza un componente
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
- Se inicializa el estado con números
[0, 1, 2, 3, 4, 5]
. FlatList
muestra estos números como imágenes, generando la URL connumber
.- Al acercarse al final de la lista (
onEndReached
), se ejecutaloadMore
:- Se agregan 5 números más al estado después de 3 segundos.
- Durante la carga, se muestra un
ActivityIndicator
al final de la lista. - 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
ywidth
) 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
yImageStyle
: 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);
-
animatedOpacity
yfadeIn
: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). -
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
.
- Estado que indica si la imagen está en proceso de carga (
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>
);
-
Contenedor principal (
View
):- Centra vertical y horizontalmente el contenido.
-
Indicador de carga (
ActivityIndicator
):- Muestra un spinner mientras la imagen se está cargando.
- Está posicionado en el centro mediante
position: 'absolute'
.
-
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
afalse
para ocultar el indicador de carga.
style
:- Combina el estilo recibido en las props con el estilo animado de opacidad (
{ opacity: animatedOpacity }
).
- Combina el estilo recibido en las props con el estilo animado de opacidad (
5. Flujo del componente
- Cuando el componente se monta, el estado
isLoading
está entrue
y se muestra elActivityIndicator
. - La imagen comienza a cargarse desde la URL proporcionada.
- Una vez que la imagen termina de cargar (
onLoadEnd
):- Se oculta el
ActivityIndicator
(setIsLoading(false)
). - Se inicia la animación de opacidad (
fadeIn
).
- Se oculta el
- 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
yuseRef
: Hooks para manejar el estado del slide actual y referenciar laFlatList
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 componenteFlatList
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 deFlatList
para desplazarse al slide indicado.scrollToIndex
: Método deFlatList
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
ypagingEnabled
: 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
.
- Recibe los datos del slide (
7. Flujo de la funcionalidad
- La pantalla muestra el primer slide.
- El botón “siguiente” avanza al próximo slide.
- En el último slide, el botón cambia a “finalizar”.
- 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.