Привет, ребята, в этом посте я покажу вам, как создать простой эффект смахивания с помощью карточек, как в 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 и использовать этот пост, чтобы понять, что я делал на каждом этапе. Если у вас есть вопросы, дайте мне знать!

Спасибо за прочтение!