Android VR View 入门

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 处理生命周期事件

我们需要将onPauseonResultonDestroy事件传递到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 更多资源

 

 

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值