Привет, ребята, в этом посте я покажу вам, как создать простой эффект смахивания с помощью карточек, как в Tinder. Я собираюсь использовать в основном компоненты React Native, за исключением react-native-elements для создания карточек.
Вы можете увидеть, как настроить среду разработки здесь: https://medium.com/@leonardobrunolima/react-native-tips-setting-up-your-development-environment-for-windows-d326635604ea?source= linkShare-42ccfccbb437-1535561576
TL;DR
Вы можете клонировать проект и использовать этот пост в качестве справки, но если вы хотите прочитать пост полностью, продолжайте!
Приступим к созданию нового проекта с помощью react-native cli:
$ react-native init SwipeIt --version 0.55.4
Добавьте пакет response-native-elements:
$ npm i react-native-elements
Поскольку мы не используем EXPO, мы должны установить response-native-vector-icons и связать его:
$ npm i --save react-native-vector-icons
Свяжите это:
$ react-native link react-native-vector-icons
Вы можете проверить документацию здесь: https://react-native-training.github.io/react-native-elements/docs/0.19.0/getting_started.html
Разделяем разработку по шагам:
1 - Создайте папку src и основной компонент контейнера карты:
//import liraries import React, { Component } from 'react'; import { View } from 'react-native'; // create a component class CardsContainer extends Component { renderCards() { return this.props.data.map((item, index) => { return ( <View key={item.id}> {this.props.renderCard(item)} </View> ) }); } render() { return ( <View> {this.renderCards()} </View> ); } } //make this component available to the app export default CardsContainer;
Хорошо, это основной контейнер карт, как вы можете видеть, у нас есть два свойства: this.props.data и this.props.renderCard. Метод render просто вызывает метод renderCards, и мы вызываем renderCard для каждого элемента в свойствах data. Эти реквизиты мы собираемся передать от компонента App.js.
2. Настройте App.js для создания и передачи свойств компоненту CardsContainer:
Мы собираемся использовать react-native-elements для создания карточек, вы можете проверить документацию здесь: https://react-native-training.github.io/react-native- elements / docs / 0.19.0 / card.html
На карточках нам нужно показать некоторые изображения, поэтому я получу эти изображения с unsplash.com и создам константу DATA для хранения этих изображений. Вы можете получить его откуда угодно. Давайте посмотрим на код App.js:
import React, { Component } from 'react'; import { StyleSheet, Text, View } from 'react-native'; import { Card, Button } from 'react-native-elements'; import CardsContainer from './src/CardsContainer'; const DATA = [ { id: 1, text: 'Card #1', uri: 'https://images.unsplash.com/photo-1535591273668-578e31182c4f?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=f28261f0564880c9086a57ee87a68887&auto=format&fit=crop&w=500&q=60' }, { id: 2, text: 'Card #2', uri: 'https://images.unsplash.com/photo-1535576434247-e0f50b766399?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=232f6dbab45b3f3a6f97e638c27fded2&auto=format&fit=crop&w=500&q=60' }, { id: 3, text: 'Card #3', uri: 'https://images.unsplash.com/photo-1535565454739-863432ea3c0e?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=7edfb9bc7d214dbf2c920723cb0ffce2&auto=format&fit=crop&w=500&q=60' }, { id: 4, text: 'Card #4', uri: 'https://images.unsplash.com/photo-1535546204504-586398ee6677?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=7320b162b147a94d4c41377d9035e665&auto=format&fit=crop&w=500&q=60' }, { id: 5, text: 'Card #5', uri: 'https://images.unsplash.com/photo-1535531298052-7457bcdae809?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=f15acb2aedb30131bb287589399185a2&auto=format&fit=crop&w=500&q=60' }, { id: 6, text: 'Card #6', uri: 'https://images.unsplash.com/photo-1535463731090-e34f4b5098c5?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=ebe64b284c0ccffbac6a0d7ce2c8d15a&auto=format&fit=crop&w=500&q=60' }, { id: 7, text: 'Card #7', uri: 'https://images.unsplash.com/photo-1535540707939-6b4813adb681?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=ce3177d04728f7d1811e342b47d1e391&auto=format&fit=crop&w=500&q=60' }, { id: 8, text: 'Card #8', uri: 'https://images.unsplash.com/photo-1535486509975-18366f9825df?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=ea59f63a657824d02872bb907fe85e76&auto=format&fit=crop&w=500&q=60' } ]; export default class App extends Component { renderCard(item) { return ( <Card key={item.id} title={item.text} image={{ uri: item.uri }} > <Text style={{ marginBottom: 10 }}> Testing.... </Text> <Button backgroundColor='#03A9F4' title="More details" /> </Card> ); } render() { return ( <View style={styles.container}> <CardsContainer data={DATA} renderCard={this.renderCard} /> </View> ); } } const styles = StyleSheet.create({ container: { flex: 1, } });
В основном у нас есть методы DATA и renderCard для передачи компоненту CardsContainer. Теперь посмотрим, как это выглядит в эмуляторе:
$ react-native run-android
Отлично, теперь мы можем перейти к компоненту CardsContainer и творить чудеса:
3 - Отрегулируйте положение карточек и добавьте базовую анимацию:
Во-первых, давайте выровняем карточки и создадим базовую анимацию перевода. Добавьте стиль и установите абсолютное положение, чтобы все карты были закреплены сверху. После этого ширина будет соответствовать наименьшему размеру внутри контейнера, поэтому нам нужно добавить ширину вручную, используя ширину экрана с помощью компонента Размеры:
//import liraries import React, { Component } from 'react'; import { View, StyleSheet, Dimensions } from 'react-native'; const SCREEN_WIDTH = Dimensions.get('window').width; // create a component class CardsContainer extends Component { renderCards() { return this.props.data.map((item, index) => { return ( <View style={styles.cardStyle} key={item.id} > {this.props.renderCard(item)} </View> ) }).reverse(); } render() { return ( <View> {this.renderCards()} </View> ); } } // define your styles const styles = StyleSheet.create({ cardStyle: { position: 'absolute', width: SCREEN_WIDTH }, }); //make this component available to the app export default CardsContainer;
Как видите, я вызвал метод reverse () на renderCards, иначе последнее изображение будет наверху.
Теперь у нас должно получиться что-то вроде этого:
Анимация !! Пойдем!
Я собираюсь использовать Animated и PanResponder, те же компоненты, что и в сообщении ниже. Потратьте немного времени, прочтите его и вернитесь:
Код становится большим, не волнуйтесь, вы можете клонировать его на GitHub: https://github.com/lblima/react-native-swipeit
//import liraries import React, { Component } from 'react'; import { View, StyleSheet, Dimensions, Animated, PanResponder } from 'react-native'; const SCREEN_WIDTH = Dimensions.get('window').width; // create a component class CardsContainer extends Component { constructor(props) { super(props); const position = new Animated.ValueXY(); const panResponder = PanResponder.create({ onStartShouldSetPanResponder: () => true, onPanResponderMove: (event, gesture) => { position.setValue({ x: gesture.dx, y: gesture.dy }); }, onPanResponderRelease: (event, gesture) => {} }); this.state = { panResponder, position, index: 0 }; } renderCards() { return this.props.data.map((item, index) => { //Add animation only to the card on top if (index === this.state.index) { return ( <Animated.View style={[ styles.cardStyle, this.state.position.getLayout() ]} {...this.state.panResponder.panHandlers} key={item.id} > {this.props.renderCard(item)} </Animated.View> ) } return ( <Animated.View style={styles.cardStyle} key={item.id} > {this.props.renderCard(item)} </Animated.View> ); }).reverse(); } render() { return ( <View> {this.renderCards()} </View> ); } } ... export default CardsContainer;
Это базовый код, заставляющий элемент перемещаться, когда вы перетаскиваете его на экран, теперь у нас должно быть это:
4 - Интерполяция и вращение
Следующим шагом будет поворот карты при изменении оси X, мы можем сделать это с помощью интерполяции. Это в основном означает, что всякий раз, когда вы перемещаете какой-либо пиксель по оси X, что-то делать (перемещать ось Y, менять цвет, вращать и т. Д.). Давайте сделаем это:
... //Add animation only to the card on top if (index === this.state.index) { return ( <Animated.View style={[ styles.cardStyle, this.state.position.getLayout() ]} {...this.state.panResponder.panHandlers} key={item.id} > {this.props.renderCard(item)} </Animated.View> ) } ...
В этой части кода позиция была передана в Animated.View с помощью строки: this.state.position.getLayout (), а позиция была изменена на onPanResponderMove событие. Теперь нам нужно изменить это поведение, чтобы не только передать новую позицию, но и выполнить поворот карты. Итак, давайте создадим для этого новый метод под названием getCardStyle.
... getCardStyle() { const { position } = this.state; const rotationX = SCREEN_WIDTH * 2; const rotate = position.x.interpolate({ inputRange: [-rotationX, 0, rotationX], outputRange: ['-120deg', '0deg', '120deg'] }); return { ...position.getLayout(), transform: [{ rotate }] } } renderCards() { return this.props.data.map((item, index) => { if (index < this.state.index) return null; //Add animation only to the card on top if (index === this.state.index) { return ( <Animated.View style={[ styles.cardStyle, this.getCardStyle() ]} {...this.state.panResponder.panHandlers} key={item.id} > {this.props.renderCard(item)} </Animated.View> ); } return ( <Animated.View key={item.id} style={[ styles.cardStyle, {transform: [{ rotate: '0deg'}]}, { top: 10 * (index - this.state.index) } ]} > {this.props.renderCard(item)} </Animated.View> ); }).reverse(); } ...
Теперь мы получаем стиль для первой карты, вызывая этот метод getCardStyle, который применяет интерполяцию к элементу. Всякий раз, когда вы двигаетесь по оси X, у вас есть вращение в том же направлении.
К другим карточкам мне пришлось применить {transform: [{rotate: ‘0deg’}]} из-за ошибки для Android.
Чтобы сделать UX более богатым, я добавил немного места к другим карточкам: {top: 10 * (index - this.state.index)}
Теперь у нас должно быть это:
5 - Ответ, когда пользователь отпускает карту
Следующим шагом является определение момента, когда пользователь отпускает карту, и проверка, возвращаем ли мы текущую карту в исходное положение или поднимаем следующую карту наверх. Для этого нам нужно добавить код в событие onPanResponderRelease.
Давайте создадим 3 новых метода, чтобы справиться с этим:
completeSwipe(direction) { const x = (direction === 'right' ? SCREEN_WIDTH + 50 : -SCREEN_WIDTH - 50); Animated.timing(this.state.position, { toValue: { x, y: 0 }, duration: 250 }).start(() => this.onCompleteSwipe()); } onCompleteSwipe() { this.setState({ index: this.state.index + 1 }); this.state.position.setValue({ x: 0, y: 0 }); } resetPosition() { this.state.position.setValue({ x: 0, y: 0 }); }
Метод completeSwipe будет вызываться при перетаскивании карты за пределы определенного значения по оси X. Поэтому мы создаем анимацию, чтобы убрать ее с экрана, и вызываем обратный вызов onCompleteSwipe. Метод onCompleteSwipe просто увеличивает индекс и устанавливает позицию по умолчанию для следующей карточки.
Теперь мы можем изменить конструктор, чтобы использовать эти методы:
//import liraries import React, { Component } from 'react'; import { View, StyleSheet, Dimensions, Animated, PanResponder } const SCREEN_WIDTH = Dimensions.get('window').width; const SWIPE_THRESHOLD = SCREEN_WIDTH * 0.40; ... constructor(props) { super(props); const position = new Animated.ValueXY(); const panResponder = PanResponder.create({ onStartShouldSetPanResponder: () => true, onPanResponderMove: (event, gesture) => { position.setValue({ x: gesture.dx, y: gesture.dy }); }, onPanResponderRelease: (event, gesture) => { if (gesture.dx > SWIPE_THRESHOLD) this.completeSwipe('right'); else if (gesture.dx < -SWIPE_THRESHOLD) this.completeSwipe('left'); else this.resetPosition(); } }); this.state = { panResponder, position, index: 0 }; } ...
SWIPE_THRESHOLD - это константа для расчета, как далеко пользователь может перетащить карточку, не отпуская ее, и поднять другую карточку наверх.
Почти готово, у нас должно быть вот это:
6 - Последние штрихи
Теперь давайте сделаем так, чтобы следующая карточка двигалась плавно.
Добавить анимацию при методе resetPosition:
... resetPosition() { Animated.spring(this.state.position, { toValue: { x: 0, y: 0 } }).start(); } ...
Импортируйте два компонента LayoutAnimation и UIManager.
//import liraries import React, { Component } from 'react'; import { View, StyleSheet, Dimensions, Animated, PanResponder, LayoutAnimation, UIManager } from 'react-native';
И используйте метод жизненного цикла componentWillUpdate:
... componentWillUpdate() { UIManager.setLayoutAnimationEnabledExperimental && UIManager.setLayoutAnimationEnabledExperimental(true); LayoutAnimation.spring(); } ...
Окончательный вариант:
Что ж, я знаю, что это был большой пост, но вы можете клонировать проект на GitHub и использовать этот пост, чтобы понять, что я делал на каждом этапе. Если у вас есть вопросы, дайте мне знать!
Спасибо за прочтение!