Mettre en place un ViewPager animé

Interface de Diego Velasquez développée en Flutter

Comme d’habitude, je commence par vous présenter le code initial de la page, puis je ne vous présenterai que les blocs de codes ayant une différence à chaque étape. J’accentuerai les ajouts dans les blocs en les mettant en gras.

import React from ‘react’;
import { StyleSheet, View, Text, SafeAreaView } from ‘react-native’;
export default class Movies extends React.Component {
constructor(props) {
super(props);
this.state = {
}
}
componentDidMount() {
}
render() {
return (
<SafeAreaView style={styles.container}>
<Text>C’est parti pour un ViewPager animé</Text>
</SafeAreaView>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: ‘center’,
justifyContent: ‘center’
}
});
Notre page ‘vide’
import React from ‘react’;
import { StyleSheet, View, Text, SafeAreaView, ScrollView, Dimensions } from ‘react-native’;
export default class Movies extends React.Component {
[…]
render() {
return (
<SafeAreaView style={styles.container}>
<ScrollView
horizontal={true}
showsHorizontalScrollIndicator={false}
pagingEnabled={true}>
<View style={styles.page}>
<Text>Film 1</Text>
</View>
<View style={styles.page}>
<Text>Film 2</Text>
</View>
<View style={styles.page}>
<Text>Film 3</Text>
</View>
</ScrollView>

</SafeAreaView>
);
}
}
const styles = StyleSheet.create({
[…]
page: {
width:Dimensions.get(‘window’).width,
alignItems: ‘center’,
justifyContent: ‘center’
}

});
Mise en place du pager
const movies = [{title:"Film 1"}, {title:"Film 2"}, {title:"Film 3"}];
import React from ‘react’;
[...]
const movies = [{title:"Film 1"}, {title:"Film 2"}, {title:"Film 3"}];
[...]
export default class Movies extends React.Component {
[...]
render() {
return (
<SafeAreaView style={styles.container}>
<ScrollView
[...]>
{this.getMoviePages()}
</ScrollView>
</SafeAreaView>
);
}
getMoviePages = () => {
let moviePages = [];
for (const aMovieIndex in movies) {
if (movies.hasOwnProperty(aMovieIndex)) {
const aMovie = movies[aMovieIndex];
moviePages.push( <View key={aMovieIndex.toString()} style={styles.page}>
<Text>{aMovie.title}</Text>
</View>);
}
}
return moviePages;
}

}
const movies = [{title:"Film 1"}, {title:"Film 2"}, {title:"Film 3"}, {title:"Film 4"}];
const movies = [
{
title:”Toy Story 4",
image:”https://aotb.xyz/imgs/toystory4.jpg"
},
{
title:”Joker”,
image:”https://aotb.xyz/imgs/joker.jpg"
},
{
title:”Star Wars — L’ascension de Skywalker”,
image:”https://aotb.xyz/imgs/starwars9.jpg"
},
{
title:”Alita : Battle Angel”,
image:”https://aotb.xyz/imgs/alita.jpg"
}

];
import React from 'react';
import { StyleSheet, View, Text, ScrollView, Dimensions, Image } from 'react-native';
[...]
export default class Movies extends React.Component {
[...]
getMoviePages = () => {
let moviePages = [];
for (const aMovieIndex in movies) {
if (movies.hasOwnProperty(aMovieIndex)) {
const aMovie = movies[aMovieIndex];
moviePages.push(<View key={aMovieIndex.toString()} style={styles.page}>
<Image source={{uri:aMovie.image}} size style={styles.movieImage} resizeMode='cover' />
<Text>{aMovie.title}</Text>
</View>);
}
}
return moviePages;
}
}
const styles = StyleSheet.create({
[...]
movieImage: {
width:190,
height:280
}

});
Première version de notre fiche de film
export default class Movies extends React.Component {
[...]
getMoviePages = () => {
let moviePages = [];
for (const aMovieIndex in movies) {
if (movies.hasOwnProperty(aMovieIndex)) {
const aMovie = movies[aMovieIndex];
moviePages.push(this.getMovieCard(aMovie, aMovieIndex));
}
}
return moviePages;
}
getMovieCard = (movie, index) => {
return (
<View key={index.toString()} style={styles.page}>
<Image source={{ uri: movie.image }} size style={styles.movieImage} resizeMode=’cover’ />
<Text>{movie.title}</Text>
</View>
)
}

}
import React from ‘react’;
import { StyleSheet, View, Text, ScrollView, Dimensions, Image } from ‘react-native’;
const cardWidth = 270;
[...]
export default class Movies extends React.Component {
[...]
getMovieCard = (movie, index) => {
return (
<View key={index.toString()} style={styles.page}>
<View style={styles.movieCard}>
<Image source={{ uri: movie.image }} size style={styles.movieImage} resizeMode=’cover’ />
<Text>{movie.title}</Text>
</View>
</View>
)
}
}
const styles = StyleSheet.create({
[…]
movieCard: {
width: cardWidth,
height: 460,
padding:20,
borderRadius: 20,
backgroundColor: ‘#fff’,
shadowColor: “#000”,
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.25,
shadowRadius: 3.84,
elevation: 5,
justifyContent: ‘flex-start’,
alignItems: ‘center’
}

});
Nos fiches films sont prêtes
Notre fiche est prête et correctement positionnée
import React from ‘react’;
import { StyleSheet, View, Text, SafeAreaView, ScrollView, Dimensions, Image } from ‘react-native’;
const cardWidth = 270;
const pageWidth = cardWidth + 10;
[...]
const styles = StyleSheet.create({
[...]
page: {
width: pageWidth,
alignItems: 'center',
justifyContent: 'flex-end'
},
[...]
movieTitle: {
fontSize: 16,
fontWeight: 'bold',
marginTop: 20,
textAlign:'center'
}

});
Première étape de la création du carrousel
render() {
return (
<SafeAreaView style={styles.container}>
<ScrollView
horizontal={true}
showsHorizontalScrollIndicator={false}
decelerationRate=’fast’
snapToInterval={pageWidth}
>
{this.getMoviePages()}
</ScrollView>
</SafeAreaView>
);
}
Le pager est de nouveau fonctionnel
export default class Movies extends React.Component {
[...]
render() {
const scrollViewPadding = (Dimensions.get('window').width - pageWidth) / 2;
const numberOfMovie = movies.length;
const widthOfTheCardsLine = numberOfMovie * pageWidth + 2 * scrollViewPadding;

return (
<SafeAreaView style={styles.container}>
<ScrollView
style={{paddingHorizontal:scrollViewPadding}}
horizontal={true}
showsHorizontalScrollIndicator={false}
decelerationRate='fast'
snapToInterval={pageWidth}>
<View style={{ flexDirection: 'row', width: widthOfTheCardsLine }}>
{this.getMoviePages()}
</View>
</ScrollView>
</SafeAreaView>
);
}
}
import React from ‘react’;
import { StyleSheet, View, Text, SafeAreaView, Dimensions, Image, Animated } from ‘react-native’;
[...]
export default class Movies extends React.Component {
[...]
render() {
[...]
return (
<SafeAreaView style={styles.container}>
<Animated.ScrollView
[...]>
[...]
</Animated.ScrollView>
</SafeAreaView>
)
}
[...]
getMovieCard = (movie, index) => {
return (
<View key={index.toString()} style={styles.page}>
<Animated.View style={styles.movieCard}>
[...]
</Animated.View>
</View>
)
}
}
export default class Movies extends React.Component {
constructor(props) {
super(props);
this.state = {
scrollX: new Animated.Value(0)
}
}
[...]
render() {
[...]
return (
<SafeAreaView style={styles.container}>
<Animated.ScrollView
[...]
scrollEventThrottle={16}
onScroll={Animated.event(
[
{
nativeEvent: { contentOffset: { x: this.state.scrollX } }
}
]
)}
>
[...]
</Animated.ScrollView>
</SafeAreaView>
)
}
[...]
}
export default class Movies extends React.Component {
getMovieCard = (movie, index) => {
return (
<View key={index.toString()} style={styles.page}>
<Animated.View style={[styles.movieCard, {opacity:this.getCardOpacity(index)}]}>
<Image source={{ uri: movie.image }} size style={styles.movieImage} resizeMode=’cover’ />
<Text style={styles.movieTitle}>{movie.title}</Text>
</Animated.View>
</View>
)
}
getCardOpacity = (index) => {
const { scrollX } = this.state;
const pageIndex = parseInt(index);
let inputRange = [pageWidth * (pageIndex-1), pageWidth * pageIndex, pageWidth * (pageIndex+1)];
return scrollX.interpolate({
inputRange: inputRange,
outputRange: [0.5, 1, 0.5],
extrapolate: ‘clamp’,
useNativeDriver: true
});
}

}
L’opacité se modifie correctement en fonction de la position de la carte
export default class Movies extends React.Component {
getMovieCard = (movie, index) => {
return (
<View key={index.toString()} style={styles.page}>
<Animated.View style={[styles.movieCard, {opacity:this.getCardOpacity(index), bottom:this.getBottomPositionForCard(index)}]}>
<Image source={{ uri: movie.image }} size style={styles.movieImage} resizeMode=’cover’ />
<Text style={styles.movieTitle}>{movie.title}</Text>
</Animated.View>
</View>
)
}
[...]
getBottomPositionForCard = (index) => {
const { scrollX } = this.state;
const pageIndex = parseInt(index);
let inputRange = [pageWidth * (pageIndex-1), pageWidth * pageIndex, pageWidth * (pageIndex+1)];
return scrollX.interpolate({
inputRange: inputRange,
outputRange: [-40, 10, -40],
extrapolate: ‘clamp’,
useNativeDriver: true
});
}

}
Notre carrousel est terminé
import React from ‘react’;
import { StyleSheet, View, Text, SafeAreaView, Dimensions, Image, Animated, TouchableOpacity } from ‘react-native’;
[...]
export default class Movies extends React.Component {
[...]
render() {
[...]
return (
<SafeAreaView style={styles.container}>
<Animated.ScrollView
[...]>
[...]
</Animated.ScrollView>
<TouchableOpacity style={styles.buyButton}>
<Text style={styles.buyButtonLabel}>ACHETER UN BILLET</Text>
</TouchableOpacity>

</SafeAreaView>
)
}
[...]
}
const styles = StyleSheet.create({
[...]
buyButton: {
position: 'absolute',
bottom: 60,
backgroundColor: '#000000'
},
buyButtonLabel: {
color: '#fff',
fontSize: 10,
paddingVertical: 10,
paddingHorizontal: 50
}

});
Mise en place de notre bouton d’achat
export default class Movies extends React.Component {
[...]
render() {
[...]
return (
<SafeAreaView style={styles.container}>
{this.getBackgroundStack()}
<Animated.ScrollView
[...]>
[...]
</Animated.ScrollView>
[...]
</SafeAreaView>
)
}
[...]
getBackgroundStack = () => {
let movieImageStack = [];
for (const aMovieIndex in movies) {
if (movies.hasOwnProperty(aMovieIndex)) {
const aMovie = movies[aMovieIndex];
movieImageStack.unshift(<View style={styles.backgroundView}>
<Image style={styles.backgroundImage} resizeMode='cover' source={{ uri: aMovie.image }} />
</View>);
}
}
return movieImageStack;
}

}
const styles = StyleSheet.create({
[...]
backgroundView: {
width: Dimensions.get('window').width,
height: Dimensions.get('window').height,
backgroundColor:'#fff',
position: 'absolute',
top: 0
},
backgroundImage: {
width: '100%',
height: '100%'
}

});
Ajout du fond d’écran
export default class Movies extends React.Component {
[...]
getBackgroundStack = () => {
let movieImageStack = [];
for (const aMovieIndex in movies) {
if (movies.hasOwnProperty(aMovieIndex)) {
const aMovie = movies[aMovieIndex];
movieImageStack.unshift(<Animated.View style={[styles.backgroundView, {left:this.getLeftPositionForBackground(aMovieIndex)}]}>
<Image style={styles.backgroundImage} resizeMode='cover' source={{ uri: aMovie.image }} />
</Animated.View>);
}
}
return movieImageStack;
}
getLeftPositionForBackground = (index) => {
const { scrollX } = this.state;
const pageIndex = parseInt(index);
let inputRange = [(pageWidth) * pageIndex, (pageWidth) * (pageIndex + 1)];
let outputRange = [0, -Dimensions.get('screen').width];
if (pageIndex == movies.length-1) {
outputRange = [0, 0];
}
return scrollX.interpolate({
inputRange: inputRange,
outputRange: outputRange,
extrapolate: 'clamp',
useNativeDriver: true
});
}

}
Résultat final de notre pager animé

--

--

Show Runner de projets mobiles. Développeur React Native et passionné par les challenges du monde mobile.

Love podcasts or audiobooks? Learn on the go with our new app.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store