Я изучал программирование в HackReactor, учебном лагере по разработке программного обеспечения. Работая там в качестве ТА, я решил создать игру, которую придумал, когда учился в колледже. И поскольку я знал только Javascript, React Native казался мне правильным выбором.

React Native определенно легко освоить, если у вас есть опыт работы с React. Facebook проделал огромную работу по поддержанию единообразия, с несколькими ключевыми различиями между мобильными устройствами и Интернетом, и для преодоления этих различий требуется твердое понимание того, как все работает под капотом. Один из них - это пользовательские жесты, также известные как panResponder.

Прежде чем я перейду к черту, что такое panResponder, позвольте мне немного поговорить об игре. По сути, игра представляет собой Candy Crush / Bejeweled, смешанную с покером. Каждая «плитка» на доске представляет собой карту, и цель состоит в том, чтобы соединить 5 карт и заработать очки в зависимости от рейтинга вашей покерной руки. Когда у вас заканчиваются карты в колоде, рассчитывается ваш общий счет, и если вы набираете достаточно высокий балл - вы получаете место в таблице лидеров!

Механика достаточно проста. Если вам интересно, попробуйте! Его можно скачать в AppStore и PlayStore.

Когда я разрабатывал кодовую базу для игры, я знал, что хочу, чтобы игроки соединяли свои карты перетаскиванием. Я хотел, чтобы в игру можно было играть, используя только их пальцы, и касание каждой карты по отдельности казалось слишком трудоемким. Кроме того, функция перетаскивания означала, что мне не нужно было кодировать «ошибки пользователя», так как игрок может просто поднять большой палец, чтобы отменить то, что он выделил до сих пор.

Итак, как мне подойти к написанию этой функции перетаскивания? В идеале, если бы существовал прослушиватель событий, подобный onMouseLeave, я мог бы просто написать некоторые функции для выделения каждой карты каждый раз, когда игрок «наводит указатель мыши» на новую карту. К сожалению, такого удобного слушателя в React Native нет. Поэтому мне нужно было придумать новый метод.

Боковое примечание. Существует прослушиватель React Native под названием onTouchEnd, но он срабатывает только после завершения сенсорного жеста, то есть когда большой палец игрока покидает экран. Поэтому, полагаясь только на это, я не узнаю, когда мы перетаскиваем новую карточку, и комбинируя onTouchStart и onTouchEnd не работает.

В своем упорстве я решил, что лучший способ реализовать это - это сделать следующее:

  • Динамическая запись положений x и y каждой карты при рендеринге
  • Сохраните его в состоянии родителя (игрового поля)
  • Настройте слушателя на родительском элементе для отслеживания жестов перетаскивания
  • Каждый раз, когда позиции x и y жеста перетаскивания перекрываются с положениями x и y карточки, выделите проклятую карту

Кажется запутанным. Но надо отдать должное, я пробовал все, что мог найти, прежде чем прибегнуть к этому.

Итак, я начал изучать штуку под названием panResponder. По сути, panResponder - это то, как React Native записывает жесты пользователя. В нем есть набор функций, которые запускаются при регистрации жеста пользователя, и они позволяют разработчикам настраивать то, что они хотят, чтобы происходило во время каждого запуска.

Очень распространенной практикой использования panResponder является реализация функции перетаскивания. Есть много ресурсов, которые объясняют, как это сделать, и это помогло мне лучше понять вещи. Но мне было нужно другое. Я реализовал способ проверки, когда пользователь перетаскивает новый элемент в рамках того же жеста.

Вот как я это сделал:

Во-первых, мне пришлось записать позиции x и y каждой карточки, и я не мог жестко это закодировать из-за разных размеров телефонов. Поэтому я использовал очень полезный прослушиватель событий под названием onLayout.

onLayout вызывается при монтировании соответствующего компонента View и предоставляет нам следующую информацию о представлении.

{nativeEvent: { layout: {x, y, width, height}}}

Да! Теперь у меня был доступ к информации о наших представлениях, и я знаю, что информация, которая нам нужна, находится в nativeEvent.layout. Вот часть кода:

<View style={styles.row} onLayout={this.getXandY.bind(this)}>
getXandY(e) {
    console.log(e.nativeEvent.layout)
}

Когда я записываю информацию, я получаю что-то вроде этого.

{
  width: 85,
  x: 19.666667,
  height: 65,
  y: 164.33337
}

Мы видим, что значения x и y относятся не ко всему размеру экрана, а к просмотру, который его охватывает. Это означает, что для получения абсолютных позиций мне пришлось бы произвести некоторые вычисления. К счастью, с гибкой коробкой это относительно легко. Благодаря гибкости я могу использовать API Dimensions в React Native, чтобы узнать, сколько «места» занимают мои представления. Затем мне просто пришлось провести несколько проб и ошибок, чтобы выяснить, правильно ли я отображаю позиции в состоянии.

getXandY(e) {
    let flex = height / 10.5 * 4; 
    let difference = 0;
    for (let i = this.props.tiles - 1; i >= 0; i--) {
      x = e.nativeEvent.layout.x
      y = e.nativeEvent.layout.y + flex;
      let pos = {
        x: x,
        y: y
      }
      this.props.savePositionsInParent(i, pos)
    }
  }

Пояснение к коду:

Поскольку макет моих карточек нерегулярный и состоит из чертовых шестиугольников, я убрал часть надоедливой математики, в приведенном выше коде есть все важные элементы при настройке x и y позиции для нескольких элементов.

  • 10,5 - это общее число гибкости для всего моего приложения. 4 - это гибкое число просмотров над игровым полем. Это необходимо для динамического расчета количества пикселей, которые вы хотите «сдвинуть».
  • Положение x изменять не нужно, так как мои представления всегда имеют ширину 100%. Если у вас другая ширина, вам может потребоваться математика, чтобы «нажать вправо». аналогично тому, что мы сделали для y
  • Я сохранил позиции в объекте и отправил его обратно в родительский View, теперь у меня есть информация в моем состоянии, соответствующая позициям моих карточек.

У моего родительского View теперь есть доступ к информации, которая выглядит примерно так:

this.state.cardPositions = 
{
  pos1: {
    x: 40,
    y: 150
  },
  pos2: {
    x: 40,
    y: 170
  },
  ...
}

Большой! Теперь, когда я знаю позиции каждой карты, последний шаг - это создание функциональности, которая позволяет мне знать, когда я перетаскиваю ее. Здесь в игру вступает panResponder.

По сути, мы хотим настроить наш panResponder на родительский View, который содержит всю игровую доску. Как только мы это сделаем, нам просто нужно написать нашу функциональность на двух основных триггерах. onPanResponderMove и onPanResponderRelease.

Вот код для его настройки:

export default class Game extends React.Component {
  constructor(props){
    super(props)
    this.state = {
      ...
    }
    this._panResponder = PanResponder.create({
      onMoveShouldSetPanResponder:(evt, gestureState) => true,
      onPanResponderMove: (evt, gestureState) => {
        // your code here
      },
      onPanResponderTerminate: (evt) => true,
      onPanResponderRelease: (evt, gestureState) => {
        // your code here
      }
    });
  }
}
render(){
 return (
   <View style={styles.gameBoard} {…this._panResponder.panHandlers}>
 )
}

Вот код:

onPanResponderMove: (evt, gestureState) => {
  cardPos = this.state.cardPositions;
  for(key in tileResponders) {
  if (gestureState.numberActiveTouches === 1) {
    let insideX = gestureState.moveX >= cardPos[key].x &&         gestureState.moveX <= (cardPos[key].x + 40);
    let insideY = gestureState.moveY >= cardPos[key].y && gestureState.moveY <= (cardPos[key].y + 55);
    if (insideX && insideY) {
       this.selectNewTile(key);
    }
  }
}  

onPanResponderRelease: (evt, gestureState) => {
  if (this.state.chosenCards.length === 5) {
     this.destroy();
  } else {
     this.reset();
  }
}

Пояснение к коду:

На первый взгляд это может показаться запутанным, но на самом деле это не так! Вот что происходит.

  • onPanResponderMove продолжает срабатывать, пока мы проводим мышью по экрану.
  • gestureState.numberActiveTouches - это то, как мы проверяем, что наше перетаскивающее движение является единичным, без обмана!
  • Мы перебираем наш объект cardPosition (13 карточек) и для каждой из них выполняем одну и ту же проверку.
  • gestureState.moveX / Y дает нам абсолютное положение жеста перетаскивания, поэтому мы знаем, что пользователь наводит курсор на позицию карты, если insideX и insideY оба верны
  • Мы определяем insideX / Y как истинное, когда жест больше, чем записанное положение x / y, числа 45/55 составляют «прямоугольник», вы можете просто использовать ширина и высота дочернего View, снова предоставленные native.layout
  • Если оба верны, мы выделяем проклятую карту

Вот и все. Как перетащить уникальные элементы в React Native одним жестом.

Я признаю, что эта версия не совсем эффективна, и эффективность кода вступает в игру, когда нам приходится перебирать более 13 карточек. В моем приложении я решил эту проблему, перебирая только соседние карты, поэтому не более 6, когда пользователь наводит курсор на центр.

Это потребовало большого количества журналов и тестирования консоли, чтобы она работала так же плавно, как и есть. Надеюсь, это поможет и кому-то другому.

Ознакомьтесь со второй частью моего путешествия по приложению здесь! На этот раз это забавная тема, как решать покерные руки!