Mettre en place un ViewPager animé

Interface de Diego Velasquez développée en Flutter
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’
  • showsHorizontalScrollIndicator : la valeur de cet attribut sera false afin de masquer la barre horizontale s’affichant par défaut lorsque l’on fait défiler la ScrollView ;
  • pagingEnable : la valeur de cet attibut sera true afin d’activer la pagination. Cela va permettre “d’arrêter” la ScrollView à chaque swipe sur une page.
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
  • snapToInterval : comme son nom l’indique, il permet de spécifier à la ScrollView qu’elle doit s’arrêter à tous les multiples de sa valeur;
  • decelerationRate : indique la vitesse de décélération de la ScrollView après un swipe. Cette attribut est obligatoire pour que l’attibrut snapToInterval fonctionne.
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>
)
}
}
  • La première est de changer l’opacité des fiches. La fiche principale a une opacité de 1, et les autres fiches auront une opacité de 0.5.
  • La seconde est de changer la position verticale des fiches. La fiche principale sera affichée de la même manière qu’actuellement, alors que les autres fiches seront légèrement masquées en bas. On changera donc la valeur de l’attribut bottom pour jouer sur cette position.
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é
  • Le bouton d’achat d’un billet
  • Le fond de la vue qui doit contenir l’affiche du film mis en avant
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
Axel de Sainte Marie

Axel de Sainte Marie

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