Предпосылки

Глава 1 | Привет, виртуальный мир
Глава 2 | Панорамная поездка
Глава 3 | Открытый кинотеатр
Глава 4 | Переходы и анимация
Глава 5 | Моделирование «Звездных войн »
Глава 6 | Исследование векторной графики
Глава 7 | Принципы UI / UX для VR-дизайна

Сфера действия этой главы

В этой книге останутся только 2 главы (включая эту). В этой главе мы будем вместе выполнять реальный проект. с упором на UI / UX.

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

Проект будет основан на Oculus Video с небольшими доработками и упрощениями. Мы собираемся позволить пользователю воспроизводить шесть лучших видео с Twitch в среде по своему выбору на главной панели управления. По мере того, как мы делаем это, мы больше сосредоточимся на дизайне / анимации пользовательского интерфейса для хорошего взаимодействия с пользователем, чем мы это делали до сих пор. Дело в том, чтобы связать воедино то, что мы узнали из этой книги.

Разрушение проекта

Давайте быстро обсудим, как этот проект будет работать.

Заглавная сцена

Сначала будет сцена заголовка, где пользователь может щелкнуть, чтобы продолжить:

Будет анимация входа и выхода, а фоном будет панорамная фотография.

Сцена на приборной панели

Затем будет сцена панели управления, где пользователь сможет выбрать видео Twitch и среду:

Серые кнопки меню будут вариантами источников видео. У нас будет только Twitch, но мы все равно добавим три дополнительные кнопки меню.

Фиолетовые плитки - это видео и параметры среды. Плитки изначально будут видео, а затем они будут динамически обновляться до параметров среды при нажатии фиолетовой кнопки.

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

Фиолетовые круги справа будут отслеживать прогресс этой сцены (выбор видео или выбор среды).

Как и в предыдущей сцене, в качестве фона и анимации будет панорамное фото.

Сцена видеоплеера

Наконец, будет видеопроигрыватель сцена, где пользователь сможет просмотреть 2D-видео на экране перед миром и нажать кнопку, чтобы вернуться к панель управления в задней части приложения:

Создание проекта

Давайте продолжим и создадим этот проект и настроим каталог проекта, используя нашу иерархию компонентов Scenes / Layouts / Elements.

Сначала запустите следующее, чтобы инициализировать наше приложение:

react-vr init VrVideoApp

Откройте папку проекта в редакторе кода.

Обновите каталог проекта, чтобы отразить это:

Создание компонентов титровальной сцены

Компоненты статической титровальной сцены

Перво-наперво нам нужно создать файл с именем TitleScene.js в папке Scene.

Это просто вложит наш компонент макета. А пока мы можем просто добавить оболочку для этого файла:

import React from 'react';
import {
  Text,
  View,
  VrButton
} from 'react-vr';
//Scene
class TitleScene extends React.Component {
  render() {
    return (
      //insert layout component
    )
  }
}
module.exports = TitleScene;

Давайте перейдем к файлу компонента макета для нашей сцены заголовка, который мы можем назвать TitleLayout.js (убедитесь, что он находится в папке layout).

Затем мы можем начать с оболочки нашего кода:

import React from 'react';
import {
  Text,
  View,
  VrButton
} from 'react-vr';
//Layout
class TitleLayout extends React.Component {
  render() {
    return (
      //insert elements
    )
  }
}
module.exports = TitleLayout;

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

Загрузите это изображение в высоком разрешении и сохраните его как title-background.jpg в папке static_assets.

А пока давайте просто добавим компонент Pano с изображением title-background (нам также нужно импортировать asset и Pano):

import React from 'react';
import {
  Text,
  View,
  VrButton,
  asset,
  Pano
} from 'react-vr';
//Scene
class TitleScene extends React.Component {
  render() {
    return (
      <Pano source={asset('title-background.jpg')}/>
      //insert layout component
    )
  }
}

Вы также можете обновить наш класс компонента приложения в index.vr.js до следующего:

export default class VrVideoApp extends React.Component {
  render() {
    return (
      <View>
        
      </View>
    );
  }
};

Теперь нам нужно добавить контейнер Flexbox, используя Flexbox для нашей сцены заголовка:

Контейнер будет иметь направление столбца и будет центрирован (как по вертикали, так и по горизонтали). Поэтому мы добавляем в наш рендер следующее:

<View 
  style={{
        width: 2,
        flexDirection: 'column',
        alignItems: 'stretch',
        justifyContent: 'center',
        layoutOrigin: [0.5, 0.5],
        transform: [{translate: [0, 0, -3]}]
  }}
>
  //insert elements
</View>

Теперь мы можем перейти к созданию наших элементов.

Нашими элементами будут 2 элемента Flexbox, добавленные в нашу колонку:

Двумя элементами будут заголовок и кнопка.

Компонент заголовка не будет использоваться повторно.

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

Начнем с нашего титульного компонента.

Создайте новый файл с именем Title.js в папке Elements.

Начнем с оболочки кода:

import React from 'react';
import {
  Text,
  View
} from 'react-vr';
//Element
class Title extends React.Component {
  render() {
    return (
      <View style={{ margin: 0.1, height: 1}}>
      </View>
    )
  }
}
module.exports = Title;

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

Давайте добавим текстовый компонент:

<View style={{ margin: 0.1}}>
  <Text style={{fontSize: 0.25, textAlign: 'center', color: "#FFFFFF"}}>
  VR VIDEO APP
  </Text>
</View>

Затем давайте создадим файл с именем Button.js для элемента кнопки в папке Elements.

Во-первых, мы можем добавить оболочку кода, который включает компонент View, который стилизует эту кнопку как элемент Flexbox:

import React from 'react';
import {
  Text,
  View,
  VrButton
} from 'react-vr';
//Element
class Button extends React.Component {
  render() {
    return (
      <View style={{ margin: 0.1, height: 0.3, backgroundColor: '#A482DF'}}>
      </View>
    )
  }
}
module.exports = Button;

Затем мы можем добавить компонент VrButton, чтобы позже мы могли добавить обработку событий к этой кнопке, а также текст кнопки (который мы будем передавать как свойство с именем text):

<View style={{ margin: 0.1, height: 0.3, backgroundColor: '#A482DF', borderRadius: 0.1}}>
  <VrButton>
    <Text style={{fontSize: 0.2, textAlign: 'center', color: "#FFFFFF"}}>
    {this.props.text}
    </Text>
  </VrButton>
</View>

Размещение наших компонентов

Все компоненты для нашей сцены Title были выполнены индивидуально. Теперь нам нужно их соединить.

Давайте вложим компонент TitleLayout в TitleScene.js.

Для этого мы начнем с импорта компонента TitleLayout:

import TitleLayout from './layouts/TitleLayout.js';

Затем мы можем использовать компонент и обернуть компонент View:

<View>
  <Pano source={asset('title-background.jpg')}/>
  <TitleLayout/>
</View>

Кроме того, компоненту TitleLayout будет передано свойство text (которое мы позже определим в index.vr.js), которое управляет текстом кнопки:

<TitleLayout text={this.props.text}/>

Перейдем к компоненту TitleLayou t, который находится в TitleLayout.js.

Во-первых, мы можем импортировать компоненты Title и Button:

import Title from './elements/Title.js';
import Button from './elements/Button.js';

Затем мы можем вложить их и передать текстовую опору кнопке:

<View 
  style={{
        width: 2,
        flexDirection: 'column',
        alignItems: 'stretch',
        justifyContent: 'center',
        layoutOrigin: [0.5, 0.5],
        transform: [{translate: [0, 0, -3]}]
  }}
>
  <Title/>
  <Button text={this.props.text}/>
</View>

Наконец, давайте импортируем и вложим весь компонент TitleScene в компонент приложения, находящийся в index.vr.js.

//other import
import TitleScene from './components/scenes/TitleScene.js';
export default class VrVideoApp extends React.Component {
  render() {
    return (
      <View>
        <TitleScene text={"Watch a Video"}/>
      </View>
    );
  }
};
//AppRegistry

Обратите внимание, что мы также определили текстовую опору, которая передавалась кнопке.

Тестирование нашей сцены

Давайте проверим нашу сцену.

cd в корень проекта, запустите npm start и проверьте локальный хост:

Woohoo! Мы завершили статичную титровальную сцену. А теперь давайте добавим анимацию.

Анимация наших компонентов

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

Начнем с нашего компонента заголовка, который находится в Title.js.

Сначала мы импортируем Animated и Easing:

import {
  Text,
  View,
  Animated
} from 'react-vr';
import { Easing } from 'react-native';

Затем мы обновляем текстовые теги на Animated.Text:

<Animated.Text style={{fontSize: 0.25, textAlign: 'center', color: "#FFFFFF"}}>
VR VIDEO APP
</Animated.Text>

Далее мы можем настроить оболочку локального состояния:

class Title extends React.Component {
  constructor() {
    super();
    this.state = { slideLeft: "", fadeIn: ""};
  }
  //render
}

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

Горизонтальное скольжение (slideLeft и slideRight) будет управляться преобразованием translateX. Если значение translateX отрицательное, он помещает элемент слева. Если он положительный, он помещает элемент справа. Поскольку мы хотим, чтобы слайды начинались с этих переведенных позиций и возвращались в свое нормальное положение, мы можем обновить локальное состояние следующим образом:

this.state = { slideLeft: new Animated.Value(-1), fadeIn: ""};

Когда мы изменим это значение на 0 в нашей анимации, слева будет заголовок.

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

this.state = { slideLeft: new Animated.Value(-1), fadeIn: new Animated.Value(0)};

Теперь мы можем создать оболочку хука жизненного цикла (когда компонент монтируется) и анимированную последовательность:

componentDidMount() {
  Animated.sequence([
  ]).start();
}

Затем мы хотим, чтобы все их значения в нашем локальном состоянии изменялись одновременно. Следовательно, мы можем использовать Animated.parallel и размещать в нем временную анимацию для всех наших значений:

componentDidMount() {
  Animated.sequence([
    Animated.parallel([
      Animated.timing(
        this.state.slideLeft,
        {
         toValue: 0,
         duration: 2000,
         easing: Easing.ease
        }
      ),
      Animated.timing(
        this.state.fadeIn,
        {
         toValue: 1,
         duration: 2000,
         easing: Easing.ease
        }
      )
    ])
  ]).start();
}

Последний шаг для нашего заголовка - привязать opacity и translateX во встроенном стиле к локальному состоянию:

<Animated.Text
  style={{
    fontSize: 0.25,
    textAlign: 'center',
    color: "#FFFFFF",
    opacity: this.state.fadeIn,
    transform: [
      {translateX: this.state.slideLeft}
    ]
}}>

Давайте сохраним и посмотрим, работает ли это:

Прохладный!

Давайте сделаем то же самое для нашей кнопки в Button.js.

Сначала мы импортируем Animated и Easing:

import {
  Text,
  View,
  VrButton,
  Animated
} from 'react-vr';
import { Easing } from 'react-native';

Вы можете скопировать и вставить конструктор и обработчик жизненного цикла из заголовка и обновить this.state.slideLeft до this.state.slideRight.

Мы также хотим обновить локальное состояние для значения slideRight:

this.state = { slideRight: new Animated.Value(1), fadeIn: new Animated.Value(0)};

Затем мы можем привязать встроенный стиль кнопки к локальному состоянию и изменить тег на Animated.View следующим образом:

<Animated.View
  style={{
    margin: 0.1,
    height: 0.3,
    backgroundColor: '#A482DF',
    borderRadius: 0.1,
    opacity: this.state.fadeIn,
    transform: [
      {translateX: this.state.slideRight}
    ]
  }}
>
//other stuff here
</Animated.View>

Убедимся, что это работает:

Идеально!

Давайте закончим следующие две сцены, а затем добавим логику переходов между каждой сценой.

Создание компонентов приборной панели

Эта сцена будет самой сложной частью нашего приложения. Во-первых, это будет динамическая сцена. Это означает, что мы собираемся изменить то, что отображается в этой сцене, перед переходом к другой сцене. Нам также необходимо включить Twitch API для получения 6 видео.

В этом разделе мы начнем с создания статической сцены. Затем мы добавим анимацию входа и динамический стиль на основе пользовательского ввода.

Чтобы все заработало, нам нужно будет реализовать Twitch API. Однако эта часть будет вынесена в отдельный раздел.

Поехали!

Компоненты сцены статической приборной панели

Во-первых, нам нужно создать файл для нашей сцены с именем Dashboard.js в папке сцен.

Загрузите эту фотографию и сохраните ее в папке static_assets как dashboard-background.jpg.

На данный момент у нас будет только следующий код:

import React from 'react';
import {
  Text,
  View,
  asset,
  Pano
} from 'react-vr';
//Scene
class Dashboard extends React.Component {
  render() {
    return (
      <View>
        <Pano source={asset('dashboard-background.jpg')}/>
     </View>
    )
  }
}
module.exports = Dashboard;

Прохладный! Давайте посмотрим на макет, чтобы понять, как делать компоненты макета:

Как это выразить в Flexbox?

По строкам будет 2 строки:

В нашей первой строке будет 5 элементов Flexbox, каждый из которых будет содержать свои собственные столбцы с элементами Flexbox:

Первый столбец в этой строке (слева) будет нашим компонентом MenuButtons.

Следующие три столбца будут нашим компонентом TileButtons.

Крайний правый столбец будет нашим компонентом ProgressCircles.

Вторая строка этой сцены будет нашей кнопкой, которая уже определена в Button.js.

Все это говорит о том, что мы можем создать компонент макета, который будет иметь код для создания двух крайних строк (и вложения в него элементов Flexbox) в файле с именем DashboardLayout.js внутри макетов. папка.

Вот код:

import React from 'react';
import {
  View
} from 'react-vr';
//Layout
class DashboardLayout extends React.Component {
  render() {
    return (
      <View style={{
        width: 5,
        flexDirection: 'row',
        alignItems: 'flex-start',
        justifyContent: 'center',
        layoutOrigin: [0.5, 0.5],
        transform: [{translate: [0, 0, -3]}]
      }}>
        //row 1 elements
      </View>
      <View style={{
        width: ,
        flexDirection: 'row',
        alignItems: 'flex-start',
        justifyContent: 'center',
        layoutOrigin: [0.5, 0.5],
        transform: [{translate: [0, 0, -3]}]
      }}>
        //row 2 elements
      </View>
    )
  }
}
module.exports = DashboardLayout;

Обратите внимание, что есть 2 компонента View для каждой строки в самом внешнем компоненте View. У каждого есть макет Flexbox, который определяет, что это строка и что все элементы в ней будут центрированы по горизонтали и вертикали.

Милая! Теперь перейдем к нашему первому элементу в первой строке DashboardLayout:

Создайте файл с именем MenuButtons.js в папке элементов.

Давайте создадим оболочку этого компонента с самым внешним компонентом View, который будет элементом Flexbox в строке, которую мы создали в предыдущем файле, и контейнером для столбца:

import React from 'react';
import {
  Text,
  View,
  VrButton
} from 'react-vr';
//Element
class MenuButtons extends React.Component {
render() {
    return (
      <View
        style={{
          margin: 0.1,
          width: 1,
          flexDirection: 'column',
          alignItems: 'center',
          justifyContent: 'center'
        }}
      >
      </View>
    )
  }
}
module.exports = MenuButtons;

Затем мы можем сделать 4 компонента View для кнопок в этом столбце:

<View
  style={{
    margin: 0.1,
    width: 1,
    flexDirection: 'column',
    alignItems: 'stretch',
    justifyContent: 'center'
  }}
>
  <View
    style={{
      margin: 0.1,
      height: 0.3,
      backgroundColor: "#898794"
    }}
  >
  </View>
  
  <View
    style={{
      margin: 0.1,
      height: 0.3,
      backgroundColor: "#898794"
    }}
  >
  </View>
  <View
    style={{
      margin: 0.1,
      height: 0.3,
      backgroundColor: "#898794"
    }}
  >
  </View>
  <View
    style={{
      margin: 0.1,
      height: 0.3,
      backgroundColor: "#898794"
    }}
  >
  </View>
</View>

Наконец, мы можем вставить компоненты VrButton и Text для каждой кнопки (фактический текст будет только у первой кнопки):

See GitHub Gist //click to see full code since it's a bit lengthy

На этом наш компонент MenuButtons завершен!

После этого мы можем создать файл TileButtons.js в нашей папке элементов, который будет отображать следующие 3 столбца:

Во-первых, мы можем добавить оболочку компонента и внешний контейнер строки Flexbox:

import React from 'react';
import {
  Text,
  View,
  VrButton
} from 'react-vr';
//Element
class TileButtons extends React.Component {
render() {
    return (
      <View style={{flexDirection: 'row', alignItems: 'center', justifyContent: 'center'}}>
      </View>
    )
  }
}
module.exports = TileButtons;

Это может показаться запутанным, поскольку мы уже определили контейнер строки в DashboardLayout, в который будет вложен этот компонент. Думайте об этом как о контейнере строки только для нашей кнопки плитки внутри самой внешней строки (как определено в DashboardLayout):

Затем мы добавляем 3 компонента View, которые будут элементами Flexbox в контейнерах строк и столбцов для наших кнопок плитки:

import React from 'react';
import {
  Text,
  View,
  VrButton
} from 'react-vr';
//Element
class TileButtons extends React.Component {
render() {
    return (
     <View>
        <View
          style={{
            margin: 0.1,
            width: 1,
            flexDirection: 'column',
            alignItems: 'center',
            justifyContent: 'center'
          }}
        >
        </View>
        <View
          style={{
            margin: 0.1,
            width: 1,
            flexDirection: 'column',
            alignItems: 'center',
            justifyContent: 'center'
          }}
        >
        </View>
        <View
          style={{
            margin: 0.1,
            width: 1,
            flexDirection: 'column',
            alignItems: 'center',
            justifyContent: 'center'
          }}
        >
        </View>
        
     </View>
    )
  }
}
module.exports = TileButtons;

Вот визуализация столбцов, которые мы только что определили:

Затем мы добавляем по 2 элемента в каждый столбец, которые являются компонентами View с VrButton, содержащим пустой текст:

See GitHub Gist //click to see full code since a bit lengthy

Опять же, это все вместе рендеринг следующих 3 столбцов:

На этом TileButtons пока что завершено.

Последним элементом в текущей строке будет ProgressCircle.js:

Создайте еще один файл с именем ProgressCircles.js в папке элементов.

Давайте добавим оболочку компонента, а также компонент View, который будет элементом в текущей строке и контейнером столбца для наших двух кругов:

import React from 'react';
import {
  View
} from 'react-vr';
//Element
class ProgressCircles extends React.Component {
render() {
    return (
      //Outermost View
      <View>
        //Column
        <View
          style={{
            margin: 0.1,
            width: 0.2,
            flexDirection: 'column',
            alignItems: 'center',
            justifyContent: 'center'
          }}
        >
        </View>
      </View>
    )
  }
}
module.exports = ProgressCircles;

Затем мы добавляем два круга выполнения, которые являются просто компонентами View, которые будут отображаться как круги из-за встроенного стиля:

//Outermost View
<View>
  //Column
  <View
    style={{
      margin: 0.1,
      width: 0.2,
      flexDirection: 'column',
      alignItems: 'center',
      justifyContent: 'center'
    }}
  >
    //Circle 1
    <View
      style={{
        margin: 0.1,
        width: 0.1,
        borderRadius: 0.5,
        height: 0.3,
        backgroundColor: "#DBDAF1"
      }}
    >
    </View>
    //Circle 2
    <View
      style={{
        margin: 0.1,
        width: 0.1,
        borderRadius: 0.5,
        height: 0.3,
        backgroundColor: "#DBDAF1"
      }}
    >
    </View>
  </View>
</View>

Размещение наших компонентов

Мы начинаем с DashboardLayout.js и импортируем 3 элемента, которые мы только что создали:

import MenuButtons from './elements/MenuButtons.js';
import TileButtons from './elements/TileButtons.js';
import ProgressCircles from './elements/ProgressCircles.js';

Затем мы вкладываем их в первый столбец:

<View>
  <View style={{
    width: 5,
    flexDirection: 'row',
    alignItems: 'flex-start',
    justifyContent: 'center',
    layoutOrigin: [0.5, 0.5],
    transform: [{translate: [0, 0, -3]}]
  }}>
    <MenuButtons/>
    <TileButtons/>
    <ProgressCircles/>
  </View>
  <View style={{
    width: 5,
    flexDirection: 'row',
    alignItems: 'flex-start',
    justifyContent: 'center',
    layoutOrigin: [0.5, 0.5],
    transform: [{translate: [0, 0, -3]}]
  }}>
    //row 2 elements
  </View>
</View>

Это отобразит следующее:

Затем мы можем импортировать нашу кнопку в контейнер второго ряда:

import Button from './elements/Button.js';

Затем мы вкладываем кнопку в контейнер второго ряда:

<View>
  <View style={{
    width: 5,
    flexDirection: 'row',
    alignItems: 'flex-start',
    justifyContent: 'center',
    layoutOrigin: [0.5, 0.5],
    transform: [{translate: [0, 0, -3]}]
  }}>
    <MenuButtons/>
    <TileButtons/>
    <ProgressCircles/>
  </View>
  <View style={{
    width: 5,
    flexDirection: 'row',
    alignItems: 'flex-start',
    justifyContent: 'center',
    layoutOrigin: [0.5, 0.5],
    transform: [{translate: [0, 0, -3]}]
  }}>
    <Button text={this.props.text}/>
  </View>
</View>

Напомним, текст компонента кнопки будет передан как опора.

После этого мы можем перейти к Dashboard.js и начать с импорта компонента DashboardLayout:

import DashboardLayout from './layouts/DashboardLayout.js';

Затем мы можем вложить его под тегом Pano:

<View>
  <Pano source={asset('dashboard-background.jpg')}/>
  <DashboardLayout text={this.props.text} />
</View>

Обратите внимание, что мы также передаем текстовую опору нашей кнопке.

Затем мы можем перейти к index.vr.js и импортировать компонент Dashboard:

import Dashboard from './components/scenes/Dashboard.js';

Наконец, удалите компонент TitleScene и вложите компонент Dashboard:

<View>
  <Dashboard text={"Select Environment"}/>
</View>

Я оставил компонент TitleScene в комментарии прямо перед возвратом:

export default class VrVideoApp extends React.Component {
  render() {
    //<TitleScene text={"Watch a Video"}/>
    return (...)
  }
}

Тестирование нашего компонента

Прежде всего, не забудьте удалить все комментарии, которые я включил в компоненты элемента (TileButtons, MenuButtons и ProgressCircles).

Сохраните все свои файлы и давайте обновим локальный хост:

Примечание. Кнопки на плитке (светло-фиолетовые) будут больше, поскольку я сделал этот снимок экрана перед обновлением встроенного стиля, как вы скопировали его из GitHub.

Похоже, контейнер строки с нашей кнопкой слишком низкий, поэтому давайте обновим встроенный стиль обеих строк в DashboardLayout.js:

<View>
  <View style={{
    width: 5,
    flexDirection: 'row',
    alignItems: 'flex-start',
    justifyContent: 'flex-start',
    layoutOrigin: [0.5, 0.5],
    transform: [{translate: [0, 0, -3]}],
    marginTop: -0.3
  }}>
    <MenuButtons/>
    <TileButtons/>
    <ProgressCircles/>
  </View>
  <View style={{
    width: 5,
    height: 0.5,
    flexDirection: 'row',
    alignItems: 'flex-start',
    justifyContent: 'center',
    layoutOrigin: [0.5, 0.5],
    transform: [{translate: [0, 0, -3]}],
    marginTop: -0.7
  }}>
    <Button text={this.props.text}/>
  </View>
</View>

Обратите внимание, что я только что добавил несколько отрицательных значений marginTop.

Я также собираюсь добавить отступы слева и справа к нашему компоненту кнопки, находящемуся в Button.js:

//in return
<Animated.View
  style={{
    margin: 0.1,
    paddingLeft: 0.2,
    paddingRight: 0.2,
    height: 0.3,
    backgroundColor: '#A482DF',
    borderRadius: 0.1,
    opacity: this.state.fadeIn,
    transform: [
      {translateX: this.state.slideRight}
    ]
  }}
>
  <VrButton>
    <Text
      style={{
        fontSize: 0.2,
        textAlign: 'center',
        color: "#FFFFFF"
      }}>
    {this.props.text}
    </Text>
  </VrButton>
</Animated.View>

Пойдем на локальный хост и обновим:

Выглядит хорошо!

Входная анимация

Поскольку мы повторно используем компонент кнопки, его анимация уже настроена на то, чтобы скользить справа и постепенно исчезать.

Чтобы упростить задачу, мы применим ту же анимацию, что и к нашему заголовку, ко всей первой строке DashboardLayout.

Откройте DashboardLayout.js и начнем с импорта Animated и Easing:

import {
  View,
  Animated
} from 'react-vr';
import { Easing } from 'react-native';

Затем мы добавляем локальное состояние со значениями анимации slideLeft и fadeIn:

//class starts here
constructor() {
  super();
  this.state = { slideLeft: new Animated.Value(-1), fadeIn: new Animated.Value(0)};
}

Затем мы можем добавить ловушку жизненного цикла с последовательностями анимации, чтобы изменить эти значения:

componentDidMount() {
  Animated.sequence([
    Animated.parallel([
      Animated.timing(
        this.state.slideLeft,
        {
         toValue: 0,
         duration: 2000,
         easing: Easing.ease
        }
      ),
      Animated.timing(
        this.state.fadeIn,
        {
         toValue: 1,
         duration: 2000,
         easing: Easing.ease
        }
      )
    ])
  ]).start();
}

Наконец, мы можем обновить тег компонента View для контейнера первой строки до Animated.View, а также привязать анимированные стили:

<Animated.View 
  style={{
    width: 5,
    flexDirection: 'row',
    alignItems: 'flex-start',
    justifyContent: 'flex-start',
    layoutOrigin: [0.5, 0.5],
    opacity: this.state.fadeIn,
    transform: [
      {translateX: this.state.slideLeft},
      {translateZ: -3}
    ],
    marginTop: -0.3
  }}
>
  <MenuButtons/>
  <TileButtons/>
  <ProgressCircles/>
</Animated.View>

Давайте проверим это:

Холодные бобы!

Обновление сцены при вводе пользователем

Есть несколько вещей, которые необходимо обновить в зависимости от ввода пользователя в эту сцену.

Во-первых, пользователь должен иметь возможность выбрать только одно видео (содержащееся в наших кнопках заголовка), а выбранное видео должно иметь пурпурную рамку вокруг него:

Мы также хотим, чтобы кнопка не отображалась, пока кнопка плитки не будет выбрана:

Когда пользователь нажимает кнопку отображения «Выбрать среду», кнопки плитки больше не будут содержать параметры видео, а параметры среды (панорамное фото в видеопроигрывателе). Мы не сможем справиться с этой частью, пока не создадим код Twitch API, однако текст кнопки должен измениться на «Смотреть видео».

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

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

Таким образом, круг выполнения отслеживает этап этой сцены.

Начнем с применения условного рендеринга нашей кнопки.

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

Откройте index.vr.js и давайте обновим код:

export default class VrVideoApp extends React.Component {
  render() {
    //<TitleScene showButton={true} text={"Watch a Video"}/>
    return (
      <View>
        <Dashboard showButton={false} text={"Select Environment"}/>
      </View>
    );
  }
};

В приведенном выше коде мы передаем свойство showButton кнопке как из TitleScene (закомментировано), так и из Dashboard. Мы присваиваем ему значение true в TitleScene, потому что хотим, чтобы он отображался по умолчанию. Мы присваиваем ему значение false в Dashboard, потому что не хотим, чтобы он отображался по умолчанию.

Теперь мы должны передать эту опору компоненту кнопки по пути вниз через TitleScene и Dashboard.

Следуя пути TitleScene, откройте TitleLayout.js и давайте передадим showButton компоненту кнопки:

<Button showButton={this.props.showButton} text={this.props.text}/>

Следуя пути Dashboard, откройте DashboardLayout.js и давайте передадим showButton компоненту кнопки:

<Button showButton={this.props.showButton} text={this.props.text}/>

Наконец, мы можем добавить условный рендеринг в Button.js, чтобы отображалась кнопка для запуска только в том случае, если showButton истинно:

render() {
  const showButton = this.props.showButton;
  return (
    <View>
      {showButton ? (
        <Animated.View
          style={{
            margin: 0.1,
            paddingLeft: 0.2,
            paddingRight: 0.2,
            height: 0.3,
            backgroundColor: '#A482DF',
            borderRadius: 0.1,
            opacity: this.state.fadeIn,
            transform: [
              {translateX: this.state.slideRight}
            ]
          }}
        >
          <VrButton>
            <Text
              style={{
                fontSize: 0.2,
                textAlign: 'center',
                color: "#FFFFFF"
              }}>
            {this.props.text}
            </Text>
          </VrButton>
        </Animated.View>
      ) :(
        <View></View>
      )}
    </View>
  )
}

Если мы проверим наш локальный хост, мы увидим, что кнопка больше не отображается по умолчанию, как ожидалось:

Затем давайте настроим кружки выполнения, чтобы динамические цвета фона контролировались локальным состоянием.

Откройте ProgressCircles.js.

Во-первых, мы можем добавить локальное состояние, цвет первого круга которого будет более темно-фиолетовым (того же цвета, что и кнопка), а цвет второго круга будет того же цвета, который мы видим в настоящее время:

constructor() {
  super();
  this.state = { circle1Color: "#A482DF" , circle2Color: "#DBDAF1"};
}

Затем мы просто привязываем это к нашему встроенному стилю:

backgroundColor: this.state.circle1Color
backgroundColor: this.state.circle2Color

Обновите локальный хост, чтобы увидеть следующее:

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

Откройте DashboardLayout.js.

Мы хотим переключать нашу кнопку при нажатии кнопки плитки. Оба эти компонента находятся в разных файлах, но вложены в DashboardLayout. В настоящее время DashboardLayout передает свойство showButton, которое определяет, будет ли отображаться кнопка. Следовательно, нам необходимо иметь функцию обработки событий (которая будет вызываться из кнопок в TileButtons.js) в этом файле, чтобы она могла изменять значение showButton опора, которая передается вниз.

Во-первых, давайте добавим свойство showButton в локальное состояние:

this.state = { slideLeft: new Animated.Value(-1), fadeIn: new Animated.Value(0), showButton: this.prop.showButton};
//showButton false by default

Затем мы хотим, чтобы свойство showButton, которое мы передаем элементу кнопки, было привязано к этому свойству локального состояния:

<Button showButton={this.state.showButton} text={this.props.text}/>

Затем мы пишем функцию обработки событий, которая будет передана компоненту TileButtons в качестве опоры. Эта функция обновит showButton до true:

//component lifecycle here
updateShowButton() {
  this.setState({showButton: true});
}
//render function here

Теперь передадим это компоненту TileButtons:

<TileButtons updateShowButton={this.updateShowButton.bind(this)}/>

В заключение, мы вызываем эту функцию щелчком любой из наших кнопок VrButton в TileButton.js:

<VrButton onClick={this.props.updateShowButton}>

Если мы обновим локальный хост, мы сможем увидеть это в действии:

Потрясающие! Осталось всего три вещи!

Следующая задача - сделать так, чтобы второй круг прогресса приобрел более тёмно-фиолетовый цвет, а первый круг прогресса стал белым.

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

Мы можем запустить эту задачу, сначала добавив к локальному состоянию, найденному в DashboardLayout.js:

this.state = { 
  slideLeft: new Animated.Value(-1), 
  fadeIn: new Animated.Value(0), 
  showButton: false, 
  color1: "#A482DF", 
  color2: "#DBDAF1"
};

В приведенном выше коде мы определяем два новых свойства: color1 и color2. Эти свойства будут переданы в ProgressCircles и будут управлять цветами кругов. Затем давайте добавим обработчик событий, который изменит свойства цвета, чтобы отразить текущий этап сцены:

updateScene() {
  this.setState({color1: "#DBDAF1", color2: "#A482DF"});
}

Затем мы передаем этот обработчик события компоненту Button:

<Button updateScene={this.updateScene.bind(this)} showButton={this.state.showButton} text={this.props.text}/>

После этого мы хотим, чтобы VrButton в Buttos.js вызывал этот переданный обработчик события при нажатии:

<VrButton onClick={this.props.updateScene}>

Вернувшись в DashboardLayout.js, мы можем привязать свойства color1 и colors2 в локальном состоянии к свойствам, передаваемым в ProgressCircles компонент:

<ProgressCircles color1={this.state.color1} color2={this.state.color2}/>

Последний фрагмент головоломки для этой задачи - удалить локальное состояние в ProgressCircles.js и привязать переданные свойства к встроенному стилю круга:

//remove the constructor with local state somewhere up here
//update the following in the render function
<View
  style={{
    margin: 0.1,
    width: 0.1,
    borderRadius: 0.5,
    height: 0.1,
    backgroundColor: this.props.color1
  }}
>
</View>
<View
  style={{
    margin: 0.1,
    width: 0.1,
    borderRadius: 0.5,
    height: 0.1,
    backgroundColor: this.props.color2
  }}
>
</View>

Давайте проверим локальный хост, чтобы убедиться, что эта задача выполнена:

Право на! Еще две задачи, тогда мы сможем сделать передышку.

Следующая задача довольно проста. Мы хотим обновить текст компонента кнопки на «Смотреть видео» одновременно с обновлением кругов выполнения.

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

constructor(props) {
  super(props);
  this.state = {
    slideLeft: new Animated.Value(-1),
    fadeIn: new Animated.Value(0),
    showButton: false,
    color1: "#A482DF",
    color2: "#DBDAF1",
    text: this.props.text
  };
}

В приведенном выше коде мы теперь передаем унаследованные свойства конструктору, чтобы мы могли установить начальное свойство text локального состояния равным this.props.text.

Затем мы можем обновить это в нашем обработчике событий updateScene:

updateScene() {
  this.setState({color1: "#DBDAF1", color2: "#A482DF", text: "Watch Video"});
}

Наконец, мы привязываем текстовую опору Button к локальному состоянию вместо того, чтобы просто передавать унаследованную опору:

<Button updateScene={this.updateScene.bind(this)} showButton={this.state.showButton} text={this.state.text}/>

Давайте закружим:

Woohoo! Еще одна задача!

Наша последняя задача по обновлению этой сцены при вводе пользователя - добавить динамический стиль для наших кнопок плитки, чтобы при щелчке на плитке появлялась более темная фиолетовая граница (и только одна может иметь границу одновременно):

Первое, что нужно сделать для этого, - обновить встроенный стиль компонентов View прямо над VrButtons в TileButtons.js:

<View
  style={{
    margin: 0.1,
    height: 0.6,
    backgroundColor: "#CAB9E5",
    borderWidth: "0",
    borderColor: "#A482DF",
    borderStyle: "solid"
  }}
>

Поскольку для параметра borderWidth установлено значение 0, по умолчанию границы отображаться не будут. Однако, если мы отрегулируем это, граница будет отображаться.

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

как нам это сделать?

Как и в предыдущих задачах, мы будем контролировать это с помощью пропсов, переданных из компонента DashboardLayout.

Откройте DashboardLayout.js и давайте обновим локальное состояние:

this.state = {
  slideLeft: new Animated.Value(-1),
  fadeIn: new Animated.Value(0),
  showButton: false,
  color1: "#A482DF",
  color2: "#DBDAF1",
  text: this.props.text,
  borderWidths: [0, 0, 0, 0, 0, 0]
};

В приведенном выше коде мы добавили массив, который можно использовать для управления стилем значений borderWidth кнопок плитки. Каждое число в borderWidths относится к одной кнопке плитки.

Нам нужен обработчик событий, прикрепленный к кнопкам плитки, который говорит: «Привет! Плитка ___ здесь. На меня нажали, поэтому измените ширину границы, чтобы вы могли ее видеть ".

Поскольку у нас уже есть событие onClick (updateShowButton), прикрепленное к VrButton в TileButtons.js, нам нужно содержать один обработчик событий с именем updateStage, который будет отображать кнопку при первом щелчке плитки и обновлять ширину границы плитки при каждом щелчке:

//previously updateShowButton
updateStage(input) {
  if(this.state.showButton === false) {
    this.setState({showButton: true});
  }
switch (input) {
    case 1:
      this.setState({borderWidths: [0.05, 0, 0, 0, 0, 0]});
      break;
    case 2:
      this.setState({borderWidths: [0, 0.05, 0, 0, 0, 0]});
      break;
    case 3:
      this.setState({borderWidths: [0, 0, 0.05, 0, 0, 0]});
      break;
    case 4:
      this.setState({borderWidths: [0, 0, 0, 0.05, 0, 0]});
      break;
    case 5:
      this.setState({borderWidths: [0, 0, 0, 0, 0.05, 0]});
      break;
    case 6:
      this.setState({borderWidths: [0, 0, 0, 0, 0, 0.05]});
      break;
  }
}

В приведенном выше коде мы обновляем showButton до значения true, если в настоящее время он имеет значение false. Мы также используем оператор switch для обновления ширины границы, которая вызвала этот обработчик событий.

Теперь давайте передадим этот обработчик событий и свойство borderWidths в качестве свойств TileButtons:

<TileButtons updateStage={this.updateStage.bind(this)} borderWidths={this.state.borderWidths}/>

Затем мы можем обновить VrButtons в TileButtons.js, чтобы вызвать эту опору обработчика событий, которая была передана вниз, и передать ввод (ключ / индекс):

<VrButton onClick={ () => this.props.updateStage(1) }>
//update input value for each VrButton

Наконец, мы можем привязать значения borderWidth для каждой кнопки плитки к значениям в свойстве borderWidths:

<View
  style={{
    margin: 0.1,
    height: 0.6,
    backgroundColor: "#CAB9E5",
    borderWidth: this.props.borderWidths[1],
    borderColor: "#A482DF",
    borderStyle: "solid"
  }}
>

Вот последний код TileButtons.js и DashboardLayout.js на этом этапе.

Обновите код и посмотрим, работает ли это:

Woohoo! Мы, наконец, закончили обновление нашей сцены Dashboard на основе ввода данных пользователем.

Нам все еще нужно делать вещи с API Twitch, но вы заслуживаете похлопывания по плечу и передышки!

Окончательный код

Код всего проекта на данный момент доступен на GitHub.

Перерыв в учебе

Это определенно самая длинная глава в этой книге.

Не стесняйтесь сделать глубокий вдох, выпить кофе, потянуться, что угодно.

Если вы хотите продолжить это позже, вы можете использовать что-то вроде Pocket.

Когда вы будете готовы, мы можем продолжить работу над компонентом видеопроигрывателя, реализовать API Twitch, а затем связать переходы, чтобы завершить работу с нашим видеоприложением.

Создание компонентов видеоплеера

Компоненты статического видеоплеера

Во-первых, давайте создадим наш компонент сцены под названием VideoPlayer.js в папке сцен.

Откройте этот файл и давайте добавим код:

import React from 'react';
import {
  Text,
  View,
  asset,
  Pano
} from 'react-vr';
//Scene
class VideoPlayer extends React.Component {
  render() {
    return (
      <View>
        <Pano source={asset('title-background.jpg')}/>
      </View>
    )
  }
}
module.exports = VideoPlayer;

Помните, что источником Pano будет управлять выбор пользователя в сцене Dashboard. На данный момент мы можем просто оставить его как title-background.jpg.

Затем мы можем создать файл VideoPlayerLayout.js в папке макетов.

В этом файле нам нужен простой контейнер для элемента видеоплеера, который будет отображаться перед виртуальной сценой, и простой контейнер для отображения кнопки в задней части виртуальной сцены:

import React from 'react';
import {
  View
} from 'react-vr';
//Layout
class VideoPlayerLayout extends React.Component {
render() {
    return (
      <View>
        <View style={{
          flex: 1,
          width: 8,
          flexDirection: 'column',
          alignItems: 'stretch',
          backgroundColor: '#333333',
          layoutOrigin: [0.5, 0.5],
          transform: [{translate: [0, 0, -5]}]
        }}>
        //insert Video element
        </View>
      <View style={{
          flex: 1,
          width: 2.5,
          flexDirection: 'column',
          alignItems: 'stretch',
          layoutOrigin: [0.5, 0.5],
          transform: [{translate: [0, 0, 5]}]
        }}>
        //insert button component
        </View>
      </View>
    )
  }
}
module.exports = VideoPlayerLayout;

Обратите внимание, что мы используем transform: [{translate: [0, 0, 5]}] для рендеринга компонента кнопки в задней части виртуального мира.

Поскольку мы повторно используем компонент кнопки, нам просто нужно создать элемент видео в VideoElement.js (в папке элементов). Примечание. Я включил «Элемент» в соглашение об именах файлов, поскольку видео - это предопределенный компонент.

Откройте этот файл и замените его следующим:

import React from 'react';
import {
  Video,
  View,
  asset
} from 'react-vr';
//Element
class VideoElement extends React.Component {
  render() {
    return (
      <View style={{ margin: 0.1, height: 4}}>
        <Video style={{height: 4}} source={asset('fireplace.mp4')} />
      </View>
    )
  }
}
module.exports = VideoElement;

В приведенном выше коде мы добавляем элемент Flexbox в наш контейнер и используем компонент Video для отображения видео.

Давайте воспользуемся файлом cabin.mp4, который мы использовали в главе 3, чтобы проверить это. Вы можете скачать это видео здесь.

Прохладный! Мы можем перейти к вложению наших компонентов для этой сцены.

Размещение наших компонентов

Во-первых, мы можем вложить импорт и вложить сцену VideoPlayer в наш компонент приложения, находящийся в index.vr.js:

//other imports here
import VideoPlayer from './components/scenes/VideoPlayer.js';
export default class VrVideoApp extends React.Component {
  render() {
    //<TitleScene showButton={true} text={"Watch a Video"}/>
    //<Dashboard showButton={false} text={"Select Environment"}/>
    return (
      <View>
        <VideoPlayer showButton={true} text={"Back to Dashboard"}/>
      </View>
    );
  }
};
AppRegistry.registerComponent('VrVideoApp', () => VrVideoApp);

Обратите внимание, что я закомментировал вложенную панель инструментов и передал реквизиты в VideoPlayer, поэтому по умолчанию кнопка будет отображаться с правильным текстом.

Спустившись на следующий уровень вниз, мы импортируем и вкладываем VideoPlayerLayout в VideoPlayer.js:

import VideoPlayerLayout from './layouts/VideoPlayerLayout.js'
//Scene
class VideoPlayer extends React.Component {
  render() {
    return (
      <View>
        <Pano source={asset('title-background.jpg')}/>
        <VideoPlayerLayout showButton={this.props.showButton} text={this.props.text}/>
      </View>
    )
  }
}

Обратите внимание, что мы продолжаем передавать свойства нашему компоненту кнопки.

Давайте спустимся к VideoPlayerLayout.js и добавим наш импорт для элементов:

import VideoElement from './elements/VideoElement.js';
import Button from './elements/Button.js';

Затем давайте вложим элементы следующим образом:

<View>
  <View style={{...}}>
    <VideoElement/>
  </View>
  <View style={{...}}>
    <Button showButton={this.props.showButton} text={this.props.text}/>
  </View>
</View>

Тестирование нашей сцены

Обновите локальный хост и давайте проверим, выполняет ли рендеринг сцена нашего видеоплеера:

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

Мы можем перевернуть контейнер кнопки (находится в VideoPlayerLayout.js), добавив rotateY: -180:

<View style={{
  flex: 1,
  width: 2.5,
  flexDirection: 'column',
  alignItems: 'stretch',
  layoutOrigin: [0.5, 0.5],
  transform: [{translate: [0, 0, 5]}, {rotateY: -180}]
}}>

Если вы проверите локальный хост, мы увидим:

Мы можем обновить значение translate по оси Y, чтобы поднять контейнер кнопки:

transform: [{translate: [0, 3.5, 5]}, {rotateY: -180}]

Теперь мы можем видеть следующее с локального хоста:

Добавление анимации входа

Последний шаг в этой сцене на данный момент - добавить анимацию входа. У нашего элемента кнопки уже есть анимация входа.

Мы можем добавить следующую анимацию, чтобы компонент VideoElement постепенно появлялся (обновите VideoElement.js):

See GitHub Gist

В этом коде мы импортируем Animated и Easing, добавляем локальное состояние со значением анимации для управления непрозрачностью, привязываем встроенный стиль к этому анимированному значению и воспроизводим анимацию. когда компонент монтируется.

Если мы обновим локальный хост, мы теперь увидим это в действии:

Реализация Twitch API

Создание ключа API

Во-первых, создайте учетную запись Twitch, если вы еще этого не сделали.

Затем мы можем создать ключ API Twitch, посетив страницу подключений.

В разделе Другие подключения вы можете нажать, чтобы зарегистрировать приложение и получить ключ API.

При регистрации приложения обязательно укажите URI перенаправления как http: // localhost:

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

Мы сможем использовать этот идентификатор клиента для получения нужных нам данных.

Установка Axios

Для выполнения запросов к API мы можем использовать Axios (в частности, React Native версию).

Мы можем установить его, выполнив в командной строке следующее:

npm install react-native-axios --save

Прежде чем двигаться дальше, давайте импортируем это в index.vr.js, где мы будем выполнять вызов API:

import axios from 'react-native-axios';

Получение информации о потоках

Мы хотим получать потоковое видео с Twitch, которое можно воспроизводить из нашего видеоплеера. Для нашей панели инструментов мы хотим отобразить изображение обложки видео (превью) 6 потоков и попросить пользователя выбрать, какой из них он хочет просмотреть.

Мы будем извлекать данные потоков из нашего компонента приложения и передавать адреса обложек в нашу Dashboard в качестве опоры.

Мы хотим получить эти потоковые видео до монтирования нашего компонента приложения, поэтому мы добавляем следующее в index.vr.js:

export default class VrVideoApp extends React.Component {
  componentWillMount() {
    
  }
  //render
}

Затем давайте добавим оболочку кода Axios, которая будет запрашивать потоковое видео и их информацию:

componentWillMount() {
    axios.get('')
      .then(response => {

      })
      .catch(e => {
        console.log(error);
      });
  }

В axios.get('') мы можем поместить URL-адрес, который будет возвращать избранные потоки Twitch. URL можно найти в официальной документации Twitch API:

Зная это, давайте вставим следующий URL:

componentWillMount() {
    axios.get('https://api.twitch.tv/kraken/streams/featured')
      .then(response => {

      })
      .catch(e => {
        console.log(error);
      });
  }

Затем мы можем прикрепить некоторые параметры URL-адреса, чтобы данные возвращались только для 6 избранных потоков (по 1 для каждой плитки в нашей сцене Dashboard):

componentWillMount() {
    axios.get('https://api.twitch.tv/kraken/streams/featured?limit=6')
      .then(response => {

      })
      .catch(e => {
        console.log(error);
      });
  }

Теперь нам нужно добавить еще один параметр URL, который аутентифицирует нас для получения данных. Мы добавляем сгенерированный ранее client_id:

//update with your client_ID
componentWillMount() {
    axios.get('https://api.twitch.tv/kraken/streams/featured?limit=6&client_id=skawlpb80ixx8e9cxafxepbn66xhe1')
      .then(response => {

      })
      .catch(e => {
        console.log(error);
      });
  }

Пока наш код говорит: «Привет, Twitch! У нас есть адрес для получения 6 потоков функций и подтверждения того, что вы согласны со мной. Как только вы дадите нам то, что нам нужно, затем нам придется что-то делать с тем, что вы нам дали. Если вы поймали ошибку, просто зарегистрируйте ее для нас. ”

Давайте посмотрим, работает ли это, записав ответ:

componentWillMount() {
  axios.get('https://api.twitch.tv/kraken/streams/featured?limit=6&client_id=skawlpb80ixx8e9cxafxepbn66xhe1')
    .then(response => {
      console.log(response);
    })
    .catch(e => {
      console.log(error);
    });
}

Обновите локальный хост и проверьте консоль. Мы должны увидеть, что возвращаются следующие данные:

В полученных данных (Object) есть данные (Object) для 6 избранных потоков в массиве под названием Featured.

В одном из представленных объектов потока есть объект с именем stream, содержащий необходимую нам информацию (URL-адрес обложки / изображения для предварительного просмотра и идентификатор):

Давайте создадим две функции, которые будут собирать URL-адреса предварительного просмотра и идентификаторы потоков в массивы: gatherPreviews и gatherID.

Начиная с первого, давайте назовем его, как только мы получим ответ от вызова API:

componentWillMount() {
  axios.get('https://api.twitch.tv/kraken/streams/featured?limit=6&client_id=skawlpb80ixx8e9cxafxepbn66xhe1')
    .then(response => {
      console.log(response);
      this.gatherPreviews(response);
    })
    .catch(e => {
      console.log(error);
    });
}

Обратите внимание, что мы передаем объект ответа gatherPreviews.

Теперь давайте добавим оболочку gatherPreviews:

gatherPreviews(response) {
  
}

Затем мы хотим отобразить (просмотреть) предварительные просмотры потоков из представленных массивов в новый массив с именем previews:

gatherPreviews(response) {
  const previews = response.data.featured.map(function(feat) {
    return feat.stream.preview.large;
  });
  console.log(previews);
}

Обратите внимание, что response.data.features.map и feat.stream.preview.large были известны по данным, которые мы регистрировали в консоли.

Давайте обновим локальный хост, и мы увидим, что массив previews (который мы зарегистрировали) на самом деле хранит 6 URL-адресов изображений предварительного просмотра:

Давайте закончим извлечение данных, написав функцию gatherStreamIDs.

Сначала обновите локальное состояние следующим образом:

this.state = { previews: "", IDs: ""}

Затем мы хотим вызвать эту функцию при получении ответа от вызова API:

componentWillMount() {
  axios.get('https://api.twitch.tv/kraken/streams/featured?limit=6&client_id=skawlpb80ixx8e9cxafxepbn66xhe1')
    .then(response => {
      console.log(response);
      this.gatherPreviews(response);
      this.gatherStreamIDs(response);
    })
    .catch(e => {
      console.log(error);
    });
}

Наконец, мы добавляем функцию, которая будет собирать все идентификаторы потоков, сохранять их в новом массиве и обновлять свойство локального состояния IDs до значения этого нового массива (также называемого идентификаторами):

gatherStreamIDs(response) {
  const IDs = response.data.featured.map(function(feat) {
    return feat.stream._id;
  });
  console.log(IDs);
}

Как вы можете видеть выше, я зарегистрировал ID, который правильно сохраняет идентификаторы потоков:

Отображение изображений для предварительного просмотра

Следующим важным шагом является передача URL-адресов изображений предварительного просмотра из локального состояния в index.vr.js в компонент TileButtons, чтобы на плитках отображались изображения предварительного просмотра.

Давайте начнем с добавления локального состояния и добавим массив previews в локальное состояние в качестве свойства, также называемого previews в index.vr.js:

constructor() {
  super();
  this.state = { previews: ""}
}
//lifecycle component here
gatherPreviews(response) {
  const previews = response.data.featured.map(function(feat) {
    return feat.stream.preview.large;
  });
  this.setState({previews: previews});
}

Давайте передадим это свойство в сцену Dashboard в качестве свойства, также называемого превью:

render() {
  //<TitleScene showButton={true} text={"Watch a Video"}/>
  //<VideoPlayer showButton={true} text={"Back to Dashboard"}/>
  return (
    <View>
      <Dashboard previews={this.state.previews} showButton={false} text={"Select Environment"}/>
    </View>
  );
}

Теперь нам нужно полностью передать эту опору в компонент TileButtons.

Давайте начнем этот процесс в Dashboard.js:

<DashboardLayout previews={this.props.previews} text={this.props.text} />Putting Together Transitions & Animations

Затем мы можем перейти на следующий уровень вниз до DashboardLayout.js:

<TileButtons previews={this.props.previews} updateStage={this.updateStage.bind(this)} borderWidths={this.state.borderWidths}/>

В TileButtons.js мы в настоящее время визуализируем пустые текстовые компоненты между VrButtons:

<Text
  style={{
    fontSize: 0.2,
    textAlign: 'center',
    color: "#FFFFFF",
  }}>
</Text>

Это было необходимо для отображения контейнеров плиток (оформленных светло-фиолетовым цветом). Нам нужно заменить это компонентом изображения, источник которого привязан к одному из URL-адресов предварительного просмотра.

Прежде чем мы добавим компонент Image, давайте импортируем его:

import {
  Text,
  View,
  VrButton,
  Image
} from 'react-vr';

Затем мы можем добавить компонент изображения и привязать источник к URL-адресам предварительного просмотра (эти компоненты изображения заменят компоненты текста):

<VrButton onClick={ () => this.props.updateStage(1) }>
  <Image source={{uri: this.props.previews[0]}} style={{width: 1, height: 0.6}}/>
</VrButton>
//See GitHub Gist for all updates

Обратите внимание, что мы должны указать ширину и высоту изображения. Мы устанавливаем то же соотношение высоты и ширины, что и контейнеры.

Посмотрим, сработало ли это:

Так круто!

Последний этап в этом разделе - исправить границы:

Для этого необходимо переместить встроенные свойства стиля borderWidth и borderColor из компонентов View в компоненты Image:

<Image
  source={{uri: this.props.previews[0]}}
  style=
  {{
    width: 1,
    height: 0.6,
    borderWidth: this.props.borderWidths[0],
    borderColor: "#A482DF"
  }}
/>
//See GitHub Gist for all updates

Обновите локальный хост, и вы увидите, что это работает!

Отображение изображений окружающей среды

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

А пока мы просто хотим сосредоточиться на отображении фотографий окружающей среды.

Для начала нам нужно еще 4 панорамных фото. Добавьте панорамные фотографии из главы 2 Аризоны, Гавайев, Нью-Гэмпшира и Техаса в нашу папку static_assets.

Затем мы можем добавить в DashboardLayout.js массив с именем environment в локальном состоянии, который будет содержать имена файлов всех панорамных фотографий:

constructor(props) {
  super(props);
  this.state = {
    slideLeft: new Animated.Value(-1),
    fadeIn: new Animated.Value(0),
    showButton: false,
    color1: "#A482DF",
    color2: "#DBDAF1",
    text: this.props.text,
    borderWidths: [0, 0, 0, 0, 0, 0],
    environments: ["title-background.jpg", "dashboard-background.jpg", "Arizona.jpg", "Hawaii.jpg", "New Hampshire.jpg", "Texas.jpg"]
  };
}

Мы также добавим свойство, которое сообщит нам стадию нашей сцены (выбирает ли пользователь потоки или среды):

this.state = {
  //everything else
  stage: 1
};

В настоящее время мы просто обновляем цвета кругов выполнения и текст кнопки, когда пользователь нажимает кнопку на этапе 1 (когда отображаются изображения потока) в функции updateScene.

Мы также хотим обновить сцену из этой функции:

updateScene() {
  this.setState({color1: "#DBDAF1", color2: "#A482DF", text: "Watch Video", stage: 2});
}

Затем мы хотим передать окружения и stage в качестве свойств компонента TileButtons следующим образом:

<TileButtons
  stage={this.state.stage}
  environments={this.state.environments}
  previews={this.props.previews}
  updateStage={this.updateStage.bind(this)}
  borderWidths={this.state.borderWidths}
/>

В TileButtons.js мы будем использовать условную визуализацию для отображения фотографий предварительного просмотра потока или фотографий окружающей среды в зависимости от стадии сцены.

Для этого сначала импортируем актив:

import {
  Text,
  View,
  VrButton,
  Image,
  asset
} from 'react-vr';

Теперь мы можем сохранить опору сцены в переменной:

render() {
    const stage = this.props.stage;
    //...
}

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

{stage === 1 ? (
  <Image
    source={{uri: this.props.previews[0]}}
    style=
    {{
      width: 1,
      height: 0.6,
      borderWidth: this.props.borderWidths[0],
      borderColor: "#A482DF"
    }}
  />
): (
  <Image
    source={asset(this.props.environments[0])}
    style=
    {{
      width: 1,
      height: 0.6,
      borderWidth: this.props.borderWidths[0],
      borderColor: "#A482DF"
    }}
  />
)}
//See GitHub Gist for all updates

Приведенный выше код можно прочитать как: «Если мы находимся на первом этапе, когда пользователь выбирает поток видео, то покажите им все изображения предварительного просмотра потока. Или покажите им среду, которую они могут выбрать для отображения в качестве панорамы видеоплеера ».

Давайте обновим локальный хост и проверим это:

Милая! Как видите, рендеринг происходит очень медленно, учитывая высокое разрешение панорамных фотографий. Однако не стоит тратить время на то, чтобы это исправить.

Настройка для записи пользовательского ввода с приборной панели

Перед тем, как мы перейдем к следующему основному разделу этой главы, мы хотим выполнить еще кое-что. То есть мы хотим выполнить настройку для записи идентификатора потока и панорамного окружения, которые пользователь выбирает из сцены Dashboard, и передать их компоненту VideoPlayer.

Во-первых, давайте добавим массив IDs из функции gatherStreamIDs в локальное состояние, найденное в index.vr.js:

this.state = { previews: "", IDs: ""};
gatherStreamIDs(response) {
  const IDs = response.data.featured.map(function(feat) {
    return feat.stream._id;
  });
  this.setState({IDs: IDs});
}

Затем давайте создадим два свойства (selectedStreamID и selectedEnv), которые в конечном итоге сохранят выбор пользователя в сцене Dashboard:

this.state = { previews: "", IDs: "", selectedStreamID: "", selectedEnv: ""};

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

Мы также можем добавить обработчик событий, который будет принимать два параметра: стадию и значение. Это будет запускаться из сцены Dashboard и позволит нам захватывать вводимые пользователем данные на каждом этапе. На основе этих параметров мы можем обновить локальное состояние по мере необходимости:

captureSelection(stage, value) {
  switch (stage) {
    case 1:
      this.setState({selectedStreamID: value});
      break;
    case 2:
      this.setState({selectedEnv: value});
      break;
  }
}

Мы хотим, чтобы этот обработчик событий вызывался из функции updateScene в DashboardLayout.js. Поэтому мы передадим этот обработчик событий компоненту Dashboard в index.vr.js:

<Dashboard
  captureSelection={this.captureSelection.bind(this)} 
  previews={this.state.previews}
  showButton={false}
  text={"Select Environment"}
/>

В Dashboard.js давайте перейдем на следующий уровень вниз к компоненту DashboardLayout:

<DashboardLayout captureSelection={this.props.captureSelection} previews={this.props.previews} text={this.props.text} />

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

И последнее, что мы можем сделать. Вернувшись в index.vr.js, давайте передадим выбранный поток и выбранную фотографию среды в компонент VideoPlayer:

render() {
    //<TitleScene showButton={true} text={"Watch a Video"}/>
    //<VideoPlayer streamID={this.state.selectedStreamID} env={this.state.selectedEnv} showButton={true} text={"Back to Dashboard"}/>
    //...
}

Переходы между сценами

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

Переход от сцены 1 к сцене 2

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

Во-первых, давайте добавим свойство к локальному состоянию под названием scene в index.vr.js:

this.state = { scene: 1, previews: "", IDs: "", selectedStreamID: "", selectedEnv: ""};

Затем давайте обновим возврат с помощью условного рендеринга в зависимости от значения scene:

render() {
  const scene = this.state.scene;
  return (
    <View>
      {scene === 1 ? (
        <TitleScene showButton={true} text={"Watch a Video"}/>
      ) : (
        scene === 2 ? (
          <Dashboard
            captureSelection={this.captureSelection.bind(this)}
            previews={this.state.previews}
            showButton={false}
            text={"Select Environment"}
          />
        ) : (
          <VideoPlayer streamID={selectedStreamID} env={selectedEnv} showButton={true} text={"Back to Dashboard"}/>
        )
      )}
    </View>
  );
}

Приведенный выше код можно прочитать как: «Если мы находимся в сцене 1, покажите сцену заголовка. Или покажите сцену приборной панели для сцены 2 или сцену видеопроигрывателя для сцены 3. "

Затем давайте добавим обработчик событий, который обновит свойство scene, чтобы запрошенная сцена отобразилась следующим образом:

changeScenes(nextScene) {
  switch (nextScene) {
    case 1:
      this.setState({scene: 1});
      break;
    case 2:
      this.setState({scene: 2});
      break;
    case 3:
      this.setState({scene: 3});
      break;
  }
}

Этот обработчик событий всегда будет вызываться компонентом Button. Чтобы знать, какую сцену запросить следующей, кнопка должна знать, в какой сцене она сейчас находится.

Это означает, что мы будем больше передавать реквизиты.

Начнем с передачи обработчика событий, который мы только что создали, и свойства scene в качестве свойств компонента TitleScene:

<TitleScene
  showButton={true}
  text={"Watch a Video"}
  changeScenes={this.changeScenes.bind(this)}
  scene={this.state.scene}
/>

В TitleScene.js мы можем передать свойства компоненту TitleLayout:

<TitleLayout
  showButton={this.props.showButton}
  text={this.props.text}
  changeScenes={this.props.changeScenes}
  scene={this.props.scene}
/>

На следующем уровне TitleLayout.js мы можем передать эти свойства кнопке:

<Button
  showButton={this.props.showButton}
  text={this.props.text}
  changeScenes={this.props.changeScenes}
  scene={this.props.scene}
/>

В Button.js мы хотим отобразить кнопку, которая вызывает changeScenes с переданной запрошенной новой сценой.

Для этого у нас есть переменная nextScene, которая имеет другое значение в зависимости от текущей сцены:

render() {
    const showButton = this.props.showButton;
    const currentScene = this.props.scene;
    let nextScene;
    switch (currentScene) {
      case 1:
        nextScene = 2;
        break;
      case 2:
        nextScene = 3;
        break;
      case 3:
        nextScene = 1;
        break;
    }
    //return
}

Затем мы обновляем onClick VrButton, чтобы он был:

<VrButton onClick={() => this.props.changeScenes(nextScene)}>

Давайте обновим локальный хост и посмотрим, работает ли он:

Холодные бобы!

Переход от сцены 3 к сцене 1

От сцены 2 до сцены 3 будет больше работы, так что давайте пока ее пропустим.

Этот переход не должен быть слишком сложным.

Во-первых, давайте обновим локальное состояние в index.vr.js так, чтобы scene начинался с 3 (это для тестирования, так как в конечном итоге он будет автоматически контролироваться):

this.state = { scene: 3, previews: "", IDs: "", selectedStreamID: "", selectedEnv: ""};

Затем мы просто передаем changeScenes и scene в качестве свойств через компонент VideoPlayer и далее до кнопки:

index.vr.js

<VideoPlayer
  streamID={this.state.selectedStreamID}
  env={this.state.selectedEnv}
  showButton={true}
  text={"Back to Dashboard"}
  changeScenes={this.changeScenes.bind(this)}
  scene={this.state.scene}
/>

VideoPlayer.js

<VideoPlayerLayout
  showButton={this.props.showButton}
  text={this.props.text}
  changeScenes={this.props.changeScenes}
  scene={this.props.scene}
/>

VideoPlayerLayout.js

<Button
  showButton={this.props.showButton}
  text={this.props.text}
  changeScenes={this.props.changeScenes}
  scene={this.props.scene}
/>

Если вы проверите локальный хост, он будет работать правильно.

Переход от сцены 2 к сцене 3

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

Сделайте глубокий вдох - и вперед!

Во-первых, давайте обновим свойство scene в локальном состоянии до 1:

this.state = { scene: 3, previews: "", IDs: "", selectedStreamID: "", selectedEnv: ""};

Затем мы просто передаем changeScenes и scene в качестве свойств через компонент Dashboard и далее до кнопки:

index.vr.js

<Dashboard
  captureSelection={this.captureSelection.bind(this)}
  previews={this.state.previews}
  showButton={false}
  text={"Select Environment"}
  changeScenes={this.changeScenes.bind(this)}
  scene={this.state.scene}
/>

Dashboard.js

<DashboardLayout
  captureSelection={this.props.captureSelection}
  previews={this.props.previews}
  text={this.props.text}
  changeScenes={this.props.changeScenes}
  scene={this.props.scene}
/>

DashboardLayout.js

<Button
  updateScene={this.updateScene.bind(this)}
  showButton={this.state.showButton}
  text={this.state.text}
  changeScenes={this.props.changeScenes}
  stage={this.state.stage}
  scene={this.props.scene}
/>

Обратите внимание, что мы также передаем свойство stage, находящееся в локальном состоянии компонента DashboardLayou t. Это позволит нам иметь два отдельных события onClick в зависимости от того, на каком этапе мы находимся в этой сцене панели инструментов.

В Button.js давайте добавим условный рендеринг, чтобы у нас был другой компонент VrButton для сцены 2, поскольку он будет иметь два возможных события onClick:

{currentScene === 2 ? (
  <VrButton
    onClick={
      () => {
      }
    }
  >
    <Text
      style={{
        fontSize: 0.2,
        textAlign: 'center',
        color: "#FFFFFF"
      }}>
    {this.props.text}
    </Text>
  </VrButton>
) : (
  <VrButton onClick={() => this.props.changeScenes(nextScene)}>
    <Text
      style={{
        fontSize: 0.2,
        textAlign: 'center',
        color: "#FFFFFF"
      }}>
    {this.props.text}
    </Text>
  </VrButton>
)}

Обратите внимание, что у нас есть пустое событие onClick для сцены 2 VrButton.

Затем давайте сохраним опору stage в переменной следующим образом:

//switch statement up here
const stage = this.props.stage;
return (
  //...

Затем давайте добавим оператор switch, в котором мы либо перейдем к этапу 2 этой сцены и отобразим фотографии нашей среды, либо перейдем к сцене 3 в зависимости от значения только что созданной переменной stage:

{currentScene === 2 ? (
  <VrButton
    onClick={
      () => {
        switch (stage) {
          case 1:
            this.props.updateScene();
            break;
          case 2:
            this.props.changeScenes(nextScene);
            break;
        }
      }
    }
  >
  //...

Теперь наш код можно прочитать так: «Если мы находимся в сцене 2, тогда нам нужен VrButton, который либо перейдет на стадию 2 сцены, либо на сцену 3».

Обновите локальный хост и давайте проверим это:

Ух-ух! Мы почти закончили!

Воспроизведение выбранного видео в выбранной среде

В настоящее время мы уже выполнили некоторую настройку обработчика событий под названием captureSelection. Этот обработчик событий полностью передан в компонент DashboardLayout из компонента приложения.

captureSelection(stage, value) {
  switch (stage) {
    case 1:
      this.setState({selectedStreamID: value});
      break;
    case 2:
      this.setState({selectedEnv: value});
      break;
  }
}

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

Если мы перейдем к DashboardLayout.js, там будет обработчик событий с именем updateStage, который, по сути, отслеживает нажатие кнопки плитки и соответственно обновляет границу:

Чтобы узнать, какую границу выделить, используется число от 1 до 6. Таким образом, мы можем отслеживать выбор пользователя в этом обработчике событий.

Во-первых, давайте добавим свойство локального состояния с именем selectionIndex:

this.state = {
  slideLeft: new Animated.Value(-1),
  fadeIn: new Animated.Value(0),
  showButton: false,
  color1: "#A482DF",
  color2: "#DBDAF1",
  text: this.props.text,
  borderWidths: [0, 0, 0, 0, 0, 0],
  selectionIndex: ""
};

Затем давайте продолжим обновлять это в обработчике событий updateStage:

updateStage(input) {
  if(this.state.showButton === false) {
    this.setState({showButton: true});
  }
switch (input) {
    case 1:
      this.setState({borderWidths: [0.05, 0, 0, 0, 0, 0], selectionIndex: 1});
      break;
    case 2:
      this.setState({borderWidths: [0, 0.05, 0, 0, 0, 0], selectionIndex: 2});
      break;
    case 3:
      this.setState({borderWidths: [0, 0, 0.05, 0, 0, 0], selectionIndex: 3});
      break;
    case 4:
      this.setState({borderWidths: [0, 0, 0, 0.05, 0, 0], selectionIndex: 4});
      break;
    case 5:
      this.setState({borderWidths: [0, 0, 0, 0, 0.05, 0], selectionIndex: 5});
      break;
    case 6:
      this.setState({borderWidths: [0, 0, 0, 0, 0, 0.05], selectionIndex: 6});
      break;
  }
}

Следующим шагом является написание логики, чтобы значение selectionIndex можно было использовать в качестве значения, передаваемого в обработчик событий captureSelection, когда пользователь выбирает последний поток (первый этап) и выбирает финальную среду (второй этап).

Итак, где мы можем вызвать обработчик события captureSelection со значением selectionIndex для конца первого этапа?

Ну, а когда первая стадия переходит во вторую? Он изменяется при вызове обработчика событий с именем updateScene. Давайте вызовем captureSelection в этом обработчике событий прямо перед обновлением сцены:

updateScene() {
  this.props.captureSelection(this.state.stage, this.state.selectionIndex);
  this.setState({color1: "#DBDAF1", color2: "#A482DF", text: "Watch Video", stage: 2});
}

Обратите внимание, как мы передали текущий stage и текущий selectionIndex в captureSelection, который читается как: «Эй, обновите сцену! Я вижу, вы уловили ввод пользователя на первом этапе. Позвольте мне использовать это, чтобы обновить свойство selectedStreamID, содержащееся здесь в локальном состоянии, которое требуется видеопроигрывателю ».

К сожалению, нам нужно внести некоторые изменения в index.vr.js и DashboardLayout.js.

Сначала удалите массив сред из локального состояния DashboardLayout и переместите его в локальное состояние компонента приложения в index.vr.js:

environments: ["title-background.jpg", "dashboard-background.jpg", "Arizona.jpg", "Hawaii.jpg", "New Hampshire.jpg", "Texas.jpg"]

Вскоре в этом появится смысл. А пока давайте передадим этот массив в качестве опоры в DashboardLayout.

index.vr.js

<Dashboard
  captureSelection={this.captureSelection.bind(this)}
  previews={this.state.previews}
  environments={this.state.environments}
  showButton={false}
  text={"Select Environment"}
  changeScenes={this.changeScenes.bind(this)}
  scene={this.state.scene}
/>

Dashboard.js

<DashboardLayout
  environments={this.props.environments}
  captureSelection={this.props.captureSelection}
  previews={this.props.previews}
  text={this.props.text}
  changeScenes={this.props.changeScenes}
  scene={this.props.scene}
/>

Затем давайте изменим передачу сред в DashboardLayout.js так, чтобы они передавались от props, а не от локального состояния:

<TileButtons
  stage={this.state.stage}
  environments={this.props.environments}
  previews={this.props.previews}
  updateStage={this.updateStage.bind(this)}
  borderWidths={this.state.borderWidths}
/>

Хорошо, почему мы сделали это изменение, передав среды из компонента приложения?

Посмотрим, как мы обновляем captureSelection в index.vr.js.

Мы хотим, чтобы captureSelection использовал переданное введенное значение для выбора из массивов, содержащих идентификаторы потоков и файлы среды:

captureSelection(stage, value) {
  switch (stage) {
    case 1:
      this.setState({selectedStreamID: this.state.IDs[value-1]});
      break;
    case 2:
      this.setState({selectedEnv: this.state.environments[value-1]});
      break;
  }
}

Теперь сцена видеопроигрывателя будет передана в правильном идентификаторе потока и файле среды, который соответствует вводу пользователя. Обратите внимание, что мы делаем value-1, поскольку переданное значение - это диапазон от 1 до 6, а массивы - от 0 до 5.

Если бы среды все еще находились в компоненте DashboardLayout, это не сработало бы.

Очень много изменений! Я знаю. Подведем итоги, где мы находимся.

Если бы наша кнопка вызывала updateScene (как определено в компоненте DashboardLayout), то мы захватили бы правильный идентификатор потока, который передается в наш видеопроигрыватель.

Теперь нам нужно написать логику, чтобы наша кнопка вызывала updateScene в конце первого этапа и перешла к следующей сцене в конце второго этапа.

Для этого давайте добавим свойство stage к локальному состоянию, найденному в DashboardLayout.js, чтобы кнопка могла обрабатывать вещи соответствующим образом:

this.state = {
  slideLeft: new Animated.Value(-1),
  fadeIn: new Animated.Value(0),
  showButton: false,
  color1: "#A482DF",
  color2: "#DBDAF1",
  text: this.props.text,
  borderWidths: [0, 0, 0, 0, 0, 0],
  selectionIndex: "",
  stage: 1
};

Теперь давайте передадим это нашей кнопке:

<Button
  updateScene={this.updateScene.bind(this)}
  showButton={this.state.showButton}
  text={this.state.text}
  changeScenes={this.props.changeScenes}
  stage={this.state.stage}
  scene={this.props.scene}
/>

В Button.js сохраните унаследованную опору stage в переменной прямо над возвращаемым значением:

const stage = this.props.stage;
//return here

Затем вызовем updateScene при нажатии кнопки на первом этапе и changeScenes при щелчке на втором этапе:

{currentScene === 2 ? (
  <VrButton
    onClick={
      () => {
        switch (stage) {
          case 1:
            this.props.updateScene();
            break;
          case 2:
            this.props.changeScenes(nextScene);
        }
      }
    }
  >

Давайте посмотрим, работает ли это, зарегистрировав захваченное выделение этапа 1 в функции captureSelection в index.vr.js:

captureSelection(stage, value) {
  switch (stage) {
    case 1:
      alert(this.state.IDs[value-1]);
      this.setState({selectedStreamID: this.state.IDs[value-1]});
      break;
    case 2:
      this.setState({selectedEnv: this.state.environments[value-1]});
      break;
  }
}

Очень быстро, давайте еще исправим одну бывшую ошибку. Мы хотим получить канал, на котором размещен поток, а не идентификатор потока, чтобы воспроизвести наше видео, когда мы туда доберемся. Итак, давайте быстро обновим наш вызов API Twitch:

gatherStreamIDs(response) {
  const IDs = response.data.featured.map(function(feat) {
    return feat.stream.channel.name;
  });
  this.setState({IDs: IDs});
}

Наконец-то! Мы завершили первый захват. Давайте проверим это, обновив локальный хост:

Очень круто! Если теперь перейти на http://player.twitch.tv/?channel=beyondthesummit, то мы сможем увидеть потоковое видео:

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

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

В конце этапа 2 наша кнопка вызывает обработчик событий для смены сцен.

Поэтому давайте добавим еще один параметр к функции changeScenes в index.vr.js для selectionIndex (пользовательский ввод / выбранная кнопка плитки):

changeScenes(nextScene, selectionIndex) {
  //...
}

Давайте воспользуемся этим новым параметром для вызова captureSelection:

  changeScenes(nextScene, selectionIndex) {
    switch (nextScene) {
      case 1:
        this.setState({scene: 1});
        break;
      case 2:
        this.setState({scene: 2});
        break;
      case 3:
        this.captureSelection(2, selectionIndex);
        this.setState({scene: 3});
        break;
    }
  }

Теперь конец этапа 2 сцены Dashboard (прямо перед переходом к сцене 3) захватит ввод пользователя и извлечет правильный файл среды из массива.

Последний шаг - передать этот дополнительный параметр нашему компоненту Button.

Для этого нам нужно начать с передачи опоры selectionIndex в качестве опоры нашему компоненту Button в DashboardLayout.js:

<Button
  updateScene={this.updateScene.bind(this)}
  showButton={this.state.showButton}
  text={this.state.text}
  changeScenes={this.props.changeScenes}
  stage={this.state.stage}
  scene={this.props.scene}
  selectionIndex={this.state.selectionIndex}
/>

Сохраните эту опору в переменной внутри функции рендеринга в Button.js:

//more stuff up here
const stage = this.props.stage;
const selectionIndex = this.props.selectionIndex;
//return here

Затем наша кнопка передает эту переменную в качестве второго параметра changeScenes:

{currentScene === 2 ? (
  <VrButton
    onClick={
      () => {
        switch (stage) {
          case 1:
            this.props.updateScene();
            break;
          case 2:
            this.props.changeScenes(nextScene, selectionIndex);
        }
      }
    }
  >
//...

Откройте index.vr.js и давайте запишем в журнал этот захваченный выбор, чтобы увидеть, извлекается ли правильное значение среды:

captureSelection(stage, value) {
  switch (stage) {
    case 1:
      console.log(this.state.IDs[value-1]);
      this.setState({selectedStreamID: this.state.IDs[value-1]});
      break;
    case 2:
      console.log(this.state.environments[value-1]);
      this.setState({selectedEnv: this.state.environments[value-1]});
      break;
  }
}

Обновите свой локальный хост и проверьте консоль:

Флиппин крутые бобы! Наконец-то мы закончили со сценой панели инструментов!

Воспроизведение выбранного потока в выбранной среде

Чтобы завершить наше приложение, нам нужно взять реквизиты streamID и env, передаваемые компоненту VideoPlayer для рендеринга потокового видео в реальном времени канала Twitch и панорамной фотографии окружение в сцене видеопроигрывателя.

Сделать правильную панорамную фотографию очень просто, мы можем просто обновить следующую строку в VideoPlayer.js:

<Pano source={asset(this.props.env)}/>

Давайте обновим локальный хост и посмотрим, сработало ли это:

Очень аккуратный!

Затем мы хотим передать URL-адрес канала потока компоненту VideoElement, чтобы мы могли встроить видео в реальном времени вместо нашего камина.

В VideoPlayer.js давайте сначала создадим локальное состояние, которое будет управлять streamURL:

constructor() {
  super();
  this.state = { streamURL: "" }
}

Перед монтированием этого компонента давайте обновим streamURL:

componentWillMount() {
  this.setState({ streamURL: 'http://player.twitch.tv/?channel=' + this.props.streamID })
  //example: http://player.twitch.tv/?channel=beyondthesummit
}

Затем мы полностью передаем это в VideoElement.js:

VideoPlayer.js

<VideoPlayerLayout
  streamURL={this.state.streamURL}
  showButton={this.props.showButton}
  text={this.props.text}
  changeScenes={this.props.changeScenes}
  scene={this.props.scene}
/>

VideoPlayerLayout.js

<VideoElement streamURL={this.props.streamURL}/>

Последним шагом всего нашего приложения является воспроизведение потокового видео в реальном времени, найденного по переданному URL-адресу.

Пш! Это легко, Майк. Мы можем просто сделать это:

<Video style={{height: 4}} source={{uri: this.props.streamURL}} />

Да ладно! Вы думали, это будет так просто?

Компонент Video в React VR встраивает источник видео в простой <video> HTML-элемент. Однако в документации Twitch API сказано, что его нужно встроить в <iframe> компонент:

как нам это сделать?

Хороший вопрос. Я буквально остановился, когда писал это, и часами пытался решить эту проблему.

7 часов спустя. Мне не повезло, и эта глава подошла к неудачному концу.

Тем не менее, я все же хочу опубликовать эту главу. Причина в том, что читатель, вероятно, сможет понять эту последнюю часть.

Окончательный код

Окончательный код доступен через GitHub.

Дополнительная практика / улучшения

По общему признанию, в это приложение можно внести много улучшений:

  1. Есть ошибка, из-за которой первая кнопка плитки работает только тогда, когда открыты инструменты разработчика.
  2. Окружающие (панорамные) изображения могут быть сжаты для более быстрой загрузки.
  3. В целом, это приложение может быть лучше организовано и может применяться более модульный подход. Мне показалось, что я вложил слишком много кода в DashboardLayout.js.

Есть также несколько возможностей еще больше попрактиковаться в React VR:

  1. Используйте 3D-моделирование для панорамных сред вместо статических изображений.
  2. Включите вызовы API для загрузки видео из источников, отличных от Twitch. Ознакомьтесь с GraphQL.
  3. Добавляйте анимацию между сценами и обеспечивайте лучшую обратную связь при вводе пользователем.
  4. Реализуйте панорамные окружающие звуки для окружающей среды.
  5. Добавить анимацию загрузки.

Глава 9

Глава 9 теперь доступна.

Поддержи автора

Если вы хотите поддержать меня, когда я пишу эту книгу, вы можете купить книгу здесь.

Книга публикуется на платформе LeanPub, которая позволяет мне обновлять мою книгу по мере ее продвижения. Каждый раз, когда добавляется глава, вы будете получать уведомление по электронной почте. Книгу можно будет читать на Medium бесплатно, однако покупка ее через LeanPub позволяет загружать электронную книгу в виде файла в формате PDF, EPUB или MOBI и помогает мне в финансовом отношении.

Кроме того, я создал специальный пакет, который предоставит вам секретную ссылку на сервер Discord, где вы сможете повлиять на то, как я пишу эту книгу.

Так что, будьте любезны, купите эту книгу на LeanPub.

С уважением,
Майк Манджаларди
Основатель Coding Artist