1.前言
我们常常看到一些浏览器中嵌入VR View的广告来提升用户的体验,对于游戏的广告则体验更佳。不管是网页还是移动设备,都可以实现VR的视觉效果。
2.实现准备
2.1.下载程序
首先下载起始程序,然后我们会通过简单的方式来学习如何添加VR View。
$ git clone https://github.com/googlecodelabs/vr_view_app_101.git
2.2.拍摄并下载全景图像
拍摄360全景图片,下载Google 的Cardboard Camera,由于google商店需要设备认证,所以无法下载,但是可以百度搜索到该应用,如果没下载也可以用程序默认的图像。
拍完之后保存到电脑中,但是直接使用会有问题,显示不出图像。原因是VR View对图像配置有特定的要求。
- VR视图图像可以存储为png,jpeg或gif。我们建议您使用jpeg改善压缩率。
- 为了获得最大的兼容性和性能,图像尺寸应为2的幂(例如2048或4096)。
- 单色图像应为2:1的宽高比(例如4096 x 2048)。
- 立体图像应为1:1的纵横比(例如4096 x 4096)。
- 图像必须是等矩形图像。如果您有其他格式的图像(例如立方体贴图),则需要将其转换为等角矩形才能在VR视图中显示。
可以看到我们拍到的360°全景图像的分辨率为10420X1663,明显不符合要求,需要进行转化。
打开https://storage.googleapis.com/cardboard-camera-converter/index.html
选择复制的图像或将其放在页面上。然后下载转换后的图像并将其复制到app/src/main/assets/converted.jpg
.
2.3 将Google VR SDK添加到项目中
在Android Studio中打开app / build.gradle,然后滚动到文件底部的“依赖项”部分,然后添加google vr组件。依存关系部分应如下所示:
dependencies {
compile 'com.android.support:appcompat-v7:25.1.0'
compile 'com.android.support:design:25.1.0'
compile 'com.google.vr:sdk-audio:1.10.0'
compile 'com.google.vr:sdk-base:1.10.0'
compile 'com.google.vr:sdk-common:1.10.0'
compile 'com.google.vr:sdk-commonwidget:1.10.0'
compile 'com.google.vr:sdk-panowidget:1.10.0'
compile 'com.google.vr:sdk-videowidget:1.10.0'
}
如果android studio 版本为3.6以上,可以使用View Binding,视图绑定,可以不用重复写FindViewById,gradle的android节点下添加viewBinding的打开方式:
viewBinding {
enabled = true
}
3.将图片添加到Welcome fragment
打开app/res/layout/welcome_fragment.xml,修改ImageView为VrPanoramaView
<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"/>
添加参数private WelcomeFragmentBinding binding;在onCreateView中添加绑定视图:
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
binding = WelcomeFragmentBinding.inflate(inflater,container,false);
return binding.getRoot();
}
并且在不同的生命周期添加panoView控件对应的方法
@Override
public void onPause() {
super.onPause();
binding.panoView.pauseRendering();
}
@Override
public void onResume() {
super.onResume();
binding.panoView.resumeRendering();
}
@Override
public void onDestroyView() {
binding.panoView.shutdown();
super.onDestroyView();
binding = null;
}
4.使用异步来加载图像
由于图像很大,因此我们不想在应用程序启动时将其加载到主UI线程中。我们要异步加载它。
它需要扩展AsyncTask类,以便可以在后台线程中执行图像加载。AsyncTask的参数为:
- 用于加载图像的AssetManager。
- 使progress方法无效的参数(我们未实现)。
- 位图将值返回到主线程。
将类声明更改为:
public class ImageLoaderTask extends AsyncTask<AssetManager, Void, Bitmap> {
}
步骤就不一一介绍,详细代码如下:
public class ImageLoaderTask extends AsyncTask <AssetManager, Void, Bitmap>{
private static final String TAG = "ImageLoaderTask";
private final String assetName;
//因为加载图像时可能会破坏试图,通过弱引用,可以立即回收垃圾,而不必等到异步任务被销毁
private final WeakReference<VrPanoramaView> viewReference;
private final VrPanoramaView.Options viewOptions;
//为了避免设备旋转时重新加载图像,我们缓存最后加载的图像,我们通过加载资产的名称以及生成的位图来做到
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;
}
@Override
protected Bitmap doInBackground(AssetManager... assetManagers) {
//通过AssetManager 来获取InputStream图像,
//将输入流传给BitmapFactory以加载图片并返回到主线程中
AssetManager assetManager = assetManagers[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;
}
}
@Override
protected void onPostExecute(Bitmap bitmap) {
super.onPostExecute(bitmap);
final VrPanoramaView vw = viewReference.get();
if (vw != null && bitmap != null) {
vw.loadImageFromBitmap(bitmap, viewOptions);
}
}
}
然后在WelcomeFragment中开启后台线程,并传递参数:
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
loadPanoImage();
}
private synchronized void loadPanoImage(){
ImageLoaderTask task = backgroundImageLoaderTask;
if(task!=null&&task.isCancelled()){
task.cancel(true);
}
//从assets中获取图片的名称,然后加载它
VrPanoramaView.Options viewOption = new VrPanoramaView.Options();
viewOption.inputType = VrPanoramaView.Options.TYPE_STEREO_OVER_UNDER;
//Using the name of the image int the assets/directory
// String picName = "sample_converted.jpg";
String picName = "IMG_converted.jpg";
//创建任务
task = new ImageLoaderTask(binding.panoView,viewOption,picName);
task.execute(getActivity().getAssets());
task = backgroundImageLoaderTask;
}
5.在gorilla fragment中添加视频
捕获全景和立体声视频并非我们普通的手机可以做到,所以我们使用已经做好的关于大猩猩的视频。
5.1将视频添加到布局
打开res/layout/gorilla_fragment.xml
(使用“文本”视图)并将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"/>
5.2 初始化VrVideoView侦听器
VrVideoView需要设置一个侦听器以响应不断变化的状态。特别:
- onLoadSuccess-成功加载视频时调用。
- onLoadError-加载视频时出错。
- onClick-用户点击或单击视频视图时调用。
- onNewFrame-在播放的每个视频帧上调用。
- onCompletion-视频结束时调用。
在return语句之前,将侦听器添加到onCreateView()
// 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);
}
});
5.3 处理视频状态保存
旋转手机后,将重新创建活动视图。由于需要初始加载视频来设置搜索栏值,因此我们希望保存该值以在重新创建活动时使用。我们还希望保存视频的运行状态,因此,如果旋转手机,则如果暂停了它就不会开始播放视频。
为此,请重载方法:onSaveInstanceState()
。在课程结束时添加以下内容
@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
savedInstanceState.putLong(STATE_PROGRESS_TIME, binding.videoView.getCurrentPosition());
savedInstanceState.putLong(STATE_VIDEO_DURATION, binding.videoView.getDuration());
savedInstanceState.putBoolean(STATE_IS_PAUSED, isPaused);
super.onSaveInstanceState(savedInstanceState);
}
然后在onCreateView()
方法中添加读取已保存状态。在初始化成员变量后立即添加此代码,其中带有注释“ // Add the restore state code here.
”
// 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);
}
5.4 处理生命周期事件
我们需要将onPause,onResult和onDestroy事件传递到VrVideoView中。在类末尾添加这些方法:
@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();
}
可见时开始播放视频
5.5 可见时开始播放视频
由于我们使用的是片段,因此WelcomeFragment和GorillaFragment都只有一个活动。在切换到GorillaFragment之前,我们不希望开始加载视频。为此,请setUserVisibleHint()
在GorillaFragment中重载。片段的可见性更改时,将调用此方法。
当片段可见时,请检查是否已加载视频,否则,请开始加载。否则,由于视频不再可见,请暂停播放。
将此方法添加到类的末尾:
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if (isVisibleToUser) {
try {
if (videoWidgetView.getDuration() <= 0) {
videoWidgetView.loadVideoFromAsset("congo_2048.mp4",
new VrVideoView.Options());
}
} catch (Exception e) {
Toast.makeText(getActivity(), "Error opening video: " + e.getMessage(), Toast.LENGTH_LONG)
.show();
}
} else {
isPaused = true;
if (videoWidgetView != null) {
videoWidgetView.pauseVideo();
}
}
}
6 更多资源
- Google VR开发者网站:https://developer.google.com/vr
- VR视图:https: //developer.google.com/vr/concepts/vrview
- 有关管理图像和位图的提示:http : //developer.android.com/training/displaying-bitmaps/index.html
- Codelabs:https ://codelabs.developers.google.com