- Смартфон Android 5.0 (Lollipop) или выше, подключенный к компьютеру с помощью USB кабеля. Например, Nexus 5.
- Настольный компьютер Android Studio версии 2.0 или выше.
- Android SDK включая следующие компоненты:
- Android 6.0 (API 23)
- Google VR SDK для Android — это набор инструментов для разработки приложений с виртуальной реальностью.
- Приложение Cardboard Camera на устройстве, с помощью которого можно отснять и создать круговую панораму — фотосферу.
- Также понадобится приложение Cardboard для настройки очков виртуальной реальности.
- И наконец, сами очки виртуальной реальности Cardboard, или как их называют еще, Oculus Rift.
- Клонируем проект
- Добавим Google VR SDK в проект
- Добавим изображение в окне приветствия фрагмента
- Добавим код для управления VrPanoramaView
- Создадим новый класс
- Загрузим изображение в фоновом режиме
- Отображение изображения
- Добавим видео фрагмент с гориллами
- добавим видео в макете
- Инициализируем переменные класса
- Инициализируем SeekBar Слушатель
- Инициализируем VrVideoView Слушатель
- Обработка сохранения состояния видео
- Обработка события жизненного цикла
- Запускаем видео при открытии фрагмента
Клонируем проект
DCIM / CardboardCamera.
IMG_20160512_105228.vr.jpg
. и выглядеть примерно так.- изображения должны быть в формате PNG, JPEG или GIF. Рекомендуется использовать формат JPEG для улучшения сжатия.
- Для обеспечения максимальной совместимости и производительности, размеры изображения должны быть степенью двойки (например, 2048 или 4096).
- Моно-изображения должны иметь соотношение сторон 2:1 (например, 4096 х 2048).
- Стереоизображения должны иметь соотношение сторон 1:1 (например, 4096 х 4096).
- Изображения должны быть еквидистантной, или равнопромежуточной проекции. В такой проекции, например, отображается географическая карта мира.
- Если у вас есть изображения в других форматах , таких как cubemap , они должны быть преобразованы в еквидистантную проекцию для того , чтобы отображаться в режиме VR.
app/src/main/assets/converted.jpg
Добавим Google VR SDK в проект
include ':app' include ':gvr-android-sdk/libraries:audio' include ':gvr-android-sdk/libraries:base' include ':gvr-android-sdk/libraries:common' include ':gvr-android-sdk/libraries:commonwidget' include ':gvr-android-sdk/libraries:panowidget' include ':gvr-android-sdk/libraries:videowidget'
Добавим изображение в окне приветствия фрагмента
- compile project(‘:gvr-android-sdk/libraries:common’)
- compile project(‘:gvr-android-sdk/libraries:commonwidget’)
- compile project(‘:gvr-android-sdk/libraries:panowidget’)
Это будет выглядеть следующим образом:
dependencies { compile 'com.android.support:appcompat-v7:23.4.0' compile 'com.android.support:design:23.4.0' compile project(':gvr-android-sdk/libraries:common') compile project(':gvr-android-sdk/libraries:commonwidget') compile project(':gvr-android-sdk/libraries:panowidget') }
Затем нажмем кнопку «Sync Now», чтобы обновить проект.
<com.google.vr.sdk.widgets.pano.VrPanoramaView android:id="@+id/pano_view" android:layout_weight="5" android:layout_height="0dp" android:layout_margin="5dip" android:layout_width="match_parent" android:scrollbars="none" android:contentDescription="@string/codelab_img_description"/>
Добавим код для управления VrPanoramaView
VrPanoramaView
:private VrPanoramaView panoWidgetView;
import com.google.vr.sdk.widgets.pano.VrPanoramaView;
onCreateView ()
, инициализируем panoWidgetView.
Замените содержимое onCreateView ()
:View v = inflater.inflate(R.layout.welcome_fragment, container,false); panoWidgetView = (VrPanoramaView) v.findViewById(R.id.pano_view); return v;
OnPause (), onResume (),
и OnDestroy ()
, чтобы передать им VrPanoramaView, добавим их чуть ниже метода onCreateView ()
.@Override public void onPause() { panoWidgetView.pauseRendering(); super.onPause(); } @Override public void onResume() { panoWidgetView.resumeRendering(); super.onResume(); } @Override public void onDestroy() { // Destroy the widget and free memory. panoWidgetView.shutdown(); super.onDestroy(); }
Создадим новый класс
Выберем папку app / Java в менеджере проекта, а затем правой кнопкой мыши и Выберем New> Java Class . Назовите класс ImageLoaderTask
.
- AssetManager используемый для загрузки изображения.
- Пустой параметр для метода прогресса (который мы не используем).
- Изображение, возвращаемое обратно в основной поток.
public class ImageLoaderTask extends AsyncTask<AssetManager, Void, Bitmap> { }
Убедитесь в том, что такие классы импортируются:
import android.content.res.AssetManager; import android.graphics.Bitmap; import android.os.AsyncTask;
@Override protected Bitmap doInBackground(AssetManager... params) { return null; }
private static final String TAG = "ImageLoaderTask"; private final String assetName; private final WeakReference<VrPanoramaView> viewReference; private final VrPanoramaView.Options viewOptions;
import com.google.vr.sdk.widgets.pano.VrPanoramaView; import java.lang.ref.WeakReference;
private static WeakReference<Bitmap> lastBitmap = new WeakReference<>(null); private static String lastName;
public ImageLoaderTask(VrPanoramaView view, VrPanoramaView.Options viewOptions, String assetName) { viewReference = new WeakReference<>(view); this.viewOptions = viewOptions; this.assetName = assetName; }
Загрузим изображение в фоновом режиме
AssetManager
, чтобы получить картинку. Затем передаем его в BitmapFactory
, чтобы загрузить изображение и вернуть его обратно в основной поток. Если есть проблема, мы будем регистрировать ее и возвращать null вместо картинки. Мы проверяем последнее загруженное изображение перед открытием потока в целях экономии памяти.doInBackground (AssetManager ... PARAMS)
следующий код
:
AssetManager assetManager = params[0]; if (assetName.equals(lastName) && lastBitmap.get() != null) { return lastBitmap.get(); } try(InputStream istr = assetManager.open(assetName)) { Bitmap b = BitmapFactory.decodeStream(istr); lastBitmap = new WeakReference<>(b); lastName = assetName; return b; } catch (IOException e) { Log.e(TAG, "Could not decode default bitmap: " + e); return null; }
import android.graphics.BitmapFactory; import android.util.Log; import java.io.IOException; import java.io.InputStream;
Отображение изображения
doInBackground ()
, добавим такой код:
@Override protected void onPostExecute(Bitmap bitmap) { final VrPanoramaView vw = viewReference.get(); if (vw != null && bitmap != null) { vw.loadImageFromBitmap(bitmap, viewOptions); } }
WelcomeFragment.java.
Нам нужна новая переменная класса, объявим ее:private ImageLoaderTask backgroundImageLoaderTask;
Затем в нижней части класса добавим новый метод loadPanoImage (). Это позволит создать новую задачу загрузчика и запустить его.
private synchronized void loadPanoImage() { ImageLoaderTask task = backgroundImageLoaderTask; if (task != null && !task.isCancelled()) { // Cancel any task from a previous loading. task.cancel(true); } // pass in the name of the image to load from assets. VrPanoramaView.Options viewOptions = new VrPanoramaView.Options(); viewOptions.inputType = VrPanoramaView.Options.TYPE_STEREO_OVER_UNDER; // use the name of the image in the assets/ directory. String panoImageName = "converted.jpg"; // create the task passing the widget view and call execute to start. task = new ImageLoaderTask(panoWidgetView, viewOptions, panoImageName); task.execute(getActivity().getAssets()); backgroundImageLoaderTask = task; }
И, наконец, нам нужно привязать загрузчик к событиям жизненного цикла , чтобы начать фактическую загрузку. Во фрагменте мы добавим его в метод onActivityCreated ()
переопределим его в конце класса WelcomeFragment:
@Override public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); loadPanoImage(); }
Добавим видео фрагмент с гориллами
Панорамное видео на смартфоне создать не получится. Для этого нужна специальная 360 градусная камера, самая дешевая сейчас стоит около 300 долларов. Для этого примера, мы будем использовать готовое панорамное видео, оно лежит в папке asset с именем congo_2048.mp4. Этим видео мы заменим изображение во втором фрагменте.
VideoWidget
. Также добавим элемент управления SeekBar, чтобы мы могли управлять видео.Откроем снова файл сценария сборки build.gradle для модуля приложения. Затем в разделе зависимостей, добавим компонент VideoWidget:
anroid-sdk/libraries:videowidget
Это будет выглядеть следующим образом:
dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.support:appcompat-v7:23.4.0' compile 'com.android.support:design:23.4.0' compile project(':gvr-android-sdk/libraries:common') compile project(':gvr-android-sdk/libraries:commonwidget') compile project(':gvr-android-sdk/libraries:panowidget') compile project(':gvr-android-sdk/libraries:videowidget') }
Нажмем кнопку «Sync Now», чтобы обновить проект.
добавим видео в макете
ImageView
следующими элементами:<com.google.vr.sdk.widgets.video.VrVideoView android:id="@+id/video_view" android:layout_width="match_parent" android:scrollbars="none" android:layout_height="250dip"/> <!-- Seeking UI & progress indicator.--> <SeekBar android:id="@+id/seek_bar" style="?android:attr/progressBarStyleHorizontal" android:layout_height="32dp" android:layout_width="fill_parent"/> <TextView android:id="@+id/status_text" android:text="Loading Video..." android:layout_height="wrap_content" android:layout_width="fill_parent" android:textSize="12sp" android:paddingStart="32dp" android:paddingEnd="32dp"/>
/** * Preserve the video's state and duration when rotating the phone. This improves * performance when rotating or reloading the video. */ private static final String STATE_IS_PAUSED = "isPaused"; private static final String STATE_VIDEO_DURATION = "videoDuration"; private static final String STATE_PROGRESS_TIME = "progressTime"; /** * The video view and its custom UI elements. */ private VrVideoView videoWidgetView; /** * Seeking UI & progress indicator. The seekBar's progress value represents milliseconds in the * video. */ private SeekBar seekBar; private TextView statusText; /** * By default, the video will start playing as soon as it is loaded. */ private boolean isPaused = false;
Не забудьте добавим операторы импорта, если их еще нет:
import com.google.vr.sdk.widgets.video.VrVideoView; import android.widget.SeekBar; import android.widget.TextView;
Инициализируем переменные класса
Теперь, когда мы объявили нужные переменные, мы должны инициализировать и найти view компоненты по ID. Допишем в метод onCreateView ()
такой код:
@Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View view = inflater.inflate(R.layout.gorilla_fragment, container,false); seekBar = (SeekBar) view.findViewById(R.id.seek_bar); statusText = (TextView) view.findViewById(R.id.status_text); videoWidgetView = (VrVideoView) view.findViewById(R.id.video_view); // Add the restore state code here. // Add the seekbar listener here. // Add the VrVideoView listener here return view; }
Инициализируем SeekBar Слушатель
Добавим слушателя onCreateView () перед оператором return.
// initialize the seekbar listener seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { // if the user changed the position, seek to the new position. if (fromUser) { videoWidgetView.seekTo(progress); updateStatusText(); } } @Override public void onStartTrackingTouch(SeekBar seekBar) { // ignore for now. } @Override public void onStopTrackingTouch(SeekBar seekBar) { // ignore for now. } });
updateStatusText ()
еще не определен. Он будет добавлен позже.Инициализируем VrVideoView Слушатель
- onLoadSuccess — вызывается при успешной загрузке видео. Здесь мы устанавливаем длительность видео для компонента seekBar и активируем его. Также здесь обновляем текст состояния.
- onLoadError — вызывается при ошибке загрузки видео. Здесь создаем тост с сообщением об ошибке.
- OnClick — вызывается при касании или клике по video view. Будем приостанавливать видео и возобновлять воспроизведение поочередно. Также здесь обновляем текст состояния.
- onNewFrame — называется при воспроизведении каждого видеокадра. Здесь обновляем пользовательский интерфейс — текст состояния и текущее положение ползунка.
- onCompletion — вызывается по окончании видео. Воспроизводим видео в цикле. Этот метод также может использоваться для перемещения к следующему видео в списке воспроизведения.
// initialize the video listener videoWidgetView.setEventListener(new VrVideoEventListener() { /** * Called by video widget on the UI thread when it's done loading the video. */ @Override public void onLoadSuccess() { Log.i(TAG, "Successfully loaded video " + videoWidgetView.getDuration()); seekBar.setMax((int) videoWidgetView.getDuration()); seekBar.setEnabled(true); updateStatusText(); } /** * Called by video widget on the UI thread on any asynchronous error. */ @Override public void onLoadError(String errorMessage) { Toast.makeText( getActivity(), "Error loading video: " + errorMessage, Toast.LENGTH_LONG) .show(); Log.e(TAG, "Error loading video: " + errorMessage); } @Override public void onClick() { if (isPaused) { videoWidgetView.playVideo(); } else { videoWidgetView.pauseVideo(); } isPaused = !isPaused; updateStatusText(); } /** * Update the UI every frame. */ @Override public void onNewFrame() { updateStatusText(); seekBar.setProgress((int) videoWidgetView.getCurrentPosition()); } /** * Make the video play in a loop. This method could also be used to move to the next video in * a playlist. */ @Override public void onCompletion() { videoWidgetView.seekTo(0); } });
import com.google.vr.sdk.widgets.video.VrVideoEventListener; import android.util.Log; import android.widget.Toast;
updateStatusText ()
. Давайте сделаем это в конце класса.private void updateStatusText() { String status = (isPaused ? "Paused: " : "Playing: ") + String.format(Locale.getDefault(), "%.2f", videoWidgetView.getCurrentPosition() / 1000f) + " / " + videoWidgetView.getDuration() / 1000f + " seconds."; statusText.setText(status); }
И добавим еще один оператор импорта:
import java.util.Locale;
Обработка сохранения состояния видео
onSaveInstanceState ()
, добавим это в конце класса:@Override public void onSaveInstanceState(Bundle savedInstanceState) { savedInstanceState.putLong(STATE_PROGRESS_TIME, videoWidgetView.getCurrentPosition()); savedInstanceState.putLong(STATE_VIDEO_DURATION, videoWidgetView.getDuration()); savedInstanceState.putBoolean(STATE_IS_PAUSED, isPaused); super.onSaveInstanceState(savedInstanceState); }
onCreateView ()
. Добавим этот код сразу после инициализации переменных класса,// Добавление восстановления состояния кода здесь.
«)// initialize based on the saved state if (savedInstanceState != null) { long progressTime = savedInstanceState.getLong(STATE_PROGRESS_TIME); videoWidgetView.seekTo(progressTime); seekBar.setMax((int)savedInstanceState.getLong(STATE_VIDEO_DURATION)); seekBar.setProgress((int) progressTime); isPaused = savedInstanceState.getBoolean(STATE_IS_PAUSED); if (isPaused) { videoWidgetView.pauseVideo(); } } else { seekBar.setEnabled(false); }
Обработка события жизненного цикла
@Override public void onPause() { super.onPause(); // Prevent the view from rendering continuously when in the background. videoWidgetView.pauseRendering(); // If the video was playing when onPause() is called, the default behavior will be to pause // the video and keep it paused when onResume() is called. isPaused = true; } @Override public void onResume() { super.onResume(); // Resume the 3D rendering. videoWidgetView.resumeRendering(); // Update the text to account for the paused video in onPause(). updateStatusText(); } @Override public void onDestroy() { // Destroy the widget and free memory. videoWidgetView.shutdown(); super.onDestroy(); }
Запускаем видео при открытии фрагмента
setUserVisibleHint ()
в GorillaFragment. Этот метод будет вызываться при изменении видимости фрагмента.@Override public void setUserVisibleHint(boolean isVisibleToUser) { super.setUserVisibleHint(isVisibleToUser); if (isVisibleToUser) { try { if (videoWidgetView.getDuration() <= 0) { videoWidgetView.loadVideoFromAsset("congo_2048.mp4"); } } catch (Exception e) { Toast.makeText(getActivity(), "Error opening video: " + e.getMessage(), Toast.LENGTH_LONG) .show(); } } else { isPaused = true; if (videoWidgetView != null) { videoWidgetView.pauseVideo(); } } }
Нужна помощь((( Нужно добавить в WelcomeFragment ещё несколько изображений, чтобы можно было их выбирать. Помогите что куда вставлять (printsdoma@gmail.com)
Добрый день. Пробую запустить приложения с изображением на 360 гр. Но при запуске вылазит ошибка
FATAL EXCEPTION: main
ругается на строку
panoWidgetView.resumeRendering();
в WelcomeFragment.java. Кто-нибудь сталкивался ?
p.s. примеры кода брал с этой страницы, а так же пробовал с оф. страницы от гугла.
Спасибо за статью, но исходный код полностью: https://yadi.sk/d/H2BfOwSCtrmEA не содержит всех файлов. Хотя-бы даже «settings.gradle» и при попытке сбилдить проект выдает ошибку Error:(1, 0) Plugin with id ‘com.android.application’ not found.
Помогите в исправлении пожалуйста
загуглите ошибку — есть масса решений