google vr 入门之VrPanoramaView制作全景图列表


上图的效果想必大家在很多VR App中都见过,下面介绍一种简单的实现方法:

大家可以看到,其实只不过是一个展示图片效果的列表,但是当你滑动到某个位置停止时,对应的条目被激活了,这张图片的全景图便开始动态的显示活动了,原理就是这么简单,那我们如何实现呢?

思路:

1. 首先我们需要一个展示图片的列表(ListView、RecycleView)

2. 列表的每个item展示对应的平面图

3. 对列表进行滑动监听,停止滑动时对应条目的全景图激活显示

4. 页面加载,默认首先要显示第一个item的全景图

思路就是这么简单,这里的全景图我们需要对应的全景图控件来显示,google vr 中有对应的全景图控件VrPanoramaView,google vr 的核心是其父类VrWidgetView,VrPanoramaView和VrVideoView是一对孪生兄弟,分别用来显示全景图和播放全景视频(google vr 视频播放即VrVideoView的使用请参考google vr 入门之制作简易的VR播放器及去除界面控制按钮),今天我们用到的是全景图的显示功能,使用VrPanoramaView非常简单:

// 使用google vr VrPanoramaView 添加的
    compile 'com.google.vr:sdk-audio:1.40.0'
    compile 'com.google.vr:sdk-base:1.40.0'
    compile 'com.google.vr:sdk-panowidget:1.40.0'
我们要用的VrPanoramaView在sdk-panowidget库中,这里使用1.40.0版本是为了去除android 7.0手机没有使用google vr服务弹出警告对话框的问题( 详情请点击)。
准备工作已经完成下面开始写代码了:

展示图片的列表我这里使用RecycleView,activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.qj.vrpanoramaviewlist.MainActivity">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recycleview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
</RelativeLayout>
mRecyclerView = (RecyclerView) findViewById(R.id.recycleview);
//全景图控件初始化
 vrPanoramaView = new VrPanoramaView(this);
 vrPanoramaView.setStereoModeButtonEnabled(false);//眼镜模式按钮禁掉
 vrPanoramaView.setFullscreenButtonEnabled(false); //全屏模式按钮禁掉
 vrPanoramaView.setInfoButtonEnabled(false); //信息按钮禁掉
 vrPanoramaView.setTouchTrackingEnabled(true); //开启手触模式

 options = new VrPanoramaView.Options();
 options.inputType = VrPanoramaView.Options.TYPE_MONO;
为RecycleView准备数据,设置适配器
//准备数据,这里模拟假数据
mDatas = new ArrayList<String>();
for (int i = 'A'; i < 'M'; i++) {
    mDatas.add(String.valueOf((char) i));
}
mSize = mDatas.size();
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
mAdapter = new HomeAdapter();
mRecyclerView.setAdapter(mAdapter);
看看HomeAdapter的内容:
class HomeAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if (viewType == PANORAMA_ITEM) {
            return new VrPanoramaViewHolder(LayoutInflater.from(mContext).inflate(R.layout.item_panorama, parent, false));
        } else {
            return new FooterHolder(LayoutInflater.from(mContext).inflate(R.layout.item_footer, parent, false));
        }
    }
    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        if (mSize - position > 2) {
            VrPanoramaViewHolder h = (VrPanoramaViewHolder) holder;
            h.tv.setText(mDatas.get(position));
            h.iv.setBackgroundResource(plan[position]);
            if (isFirstTime) {//首次进入要显示第一个item的全景
                Bitmap bitmap = BitmapFactory.decodeResource(getResources(), vr[0]);
                vrPanoramaView.loadImageFromBitmap(bitmap, options);
                h.iv.addView(vrPanoramaView);
                currentPos = 0;
                isFirstTime = false;
            }
        } else if (position == mSize - 1) {
            FooterHolder h = (FooterHolder) holder;
            h.tv.setText("到底啦...");
        }
        Log.e(TAG, "onBindViewHolder: " + position);
    }
    @Override
    public int getItemCount() {
        return mSize;
    }
    @Override
    public int getItemViewType(int position) {
        if (mSize - position > 2) {
            return PANORAMA_ITEM;//全景图类型的item
        } else {
            return FOOTER_ITEM;//底部填充的两个item
        }
    }
    class VrPanoramaViewHolder extends RecyclerView.ViewHolder {//全景图的Holder
        FrameLayout iv;
        TextView tv;
        public VrPanoramaViewHolder(View view) {
            super(view);
            iv = (FrameLayout) view.findViewById(R.id.iv_content);
            tv = (TextView) view.findViewById(R.id.content_name);
        }
    }
    class FooterHolder extends RecyclerView.ViewHolder {//底部填充的Holder
        TextView tv;
        public FooterHolder(View view) {
            super(view);
            tv = (TextView) view.findViewById(R.id.footer_tv);
        }
    }
}
对RecycleView添加滚动监听:
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
    LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
    int lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition();
    int pos = lastVisibleItemPosition - 2;
    if (RecyclerView.SCROLL_STATE_IDLE == newState) {//停止滑动
        showPanorama(pos);
    } else {
        if (pos == currentPos || currentPos < 0)//目前正在显示不能移除
            return;
        ViewParent parent = vrPanoramaView.getParent();
        if (parent != null) {//防止在滑动过程中发现被复用的全景图控件
            ViewGroup viewGroup = (ViewGroup) parent;
            viewGroup.removeView(vrPanoramaView);
            currentPos = -1;//此处赋值表示全景图控件没有处在任何item的位置
        }
    }
    super.onScrollStateChanged(recyclerView, newState);
}
这是RecycleView滚动监听回调的方法,在回调中我们得到屏幕中最后一个可见的item的postion,即lastVisibleItemPosition,我们关心的不是这个条目,而是它的上上一个条目,故而才有了int pos = lastVisibleItemPosition - 2;这句代码,pos才是我所关心的(默认一屏幕最少显示三个item最多显示4个item,当前屏幕中最后一个可见的item位于屏幕中第3个或者是第4个的位置,该位置的上上一个item位于屏幕的中间靠上区域内,这个区域的条目显示全景图,用户看着会比较舒服),当停止滑动后,显示全景图:
private void showPanorama(int pos) {
    if (pos == currentPos)//防止重复,做无用功
        return;
    HomeAdapter.VrPanoramaViewHolder childViewHolder = (HomeAdapter.VrPanoramaViewHolder) mRecyclerView.findViewHolderForAdapterPosition(pos);
    ViewParent parent = vrPanoramaView.getParent();
    if (parent != null) {
        ViewGroup viewGroup = (ViewGroup) parent;
        viewGroup.removeView(vrPanoramaView);
    }
    Bitmap bitmap = BitmapFactory.decodeResource(getResources(), vr[pos]);
    vrPanoramaView.loadImageFromBitmap(bitmap, options);
    childViewHolder.iv.addView(vrPanoramaView);
    currentPos = pos;
}
该方法是显示RecycleView对应位置item的全景图,全景图也是一个View,只需要通过addView的形式将其添加到对应的item上即可,添加之前加载该item的全景图片。
以上是根据上面提到的思路贴出了对应的代码以及对代码做了简要的分析,实现一个全景图列表其实就是这么简单,这只是一篇入门级别的文章,我们的全景图列表是在Activity中做的,没有搭建项目框架,如果在Fragment页面中加载会比这个复杂一些,后期文章会继续跟进,欢迎大家继续关注!

.apk下载


源码下载

以下是一个简单的C++实现,使用GDI+将平面图像映射到VR全景图中: ```c++ #include <iostream> #include <fstream> #include <string> #include <vector> #include <cmath> #include <windows.h> #include <gdiplus.h> using namespace Gdiplus; #pragma comment (lib,"Gdiplus.lib") // 定义图像元素类 class ImageElement { public: ImageElement(std::string filename, int x, int y) : m_filename(filename), m_x(x), m_y(y) {} // 加载图像 bool load() { Bitmap bmp(m_filename.c_str()); if (bmp.GetLastStatus() != Ok) { return false; } m_width = bmp.GetWidth(); m_height = bmp.GetHeight(); m_pixels.resize(m_width * m_height); for (int i = 0; i < m_width; i++) { for (int j = 0; j < m_height; j++) { Color color; bmp.GetPixel(i, j, &color); m_pixels[j * m_width + i] = color.GetValue(); } } return true; } // 获得像素 unsigned int getPixel(int x, int y) const { return m_pixels[y * m_width + x]; } int getWidth() const { return m_width; } int getHeight() const { return m_height; } int getX() const { return m_x; } int getY() const { return m_y; } private: std::string m_filename; // 图像文件名 int m_x, m_y; // 在VR全景图中的起始坐标 int m_width, m_height; // 图像宽度和高度 std::vector<unsigned int> m_pixels; // 像素值 }; // 定义VR全景图类 class VRPanorama { public: VRPanorama(int width, int height, std::string filename) : m_width(width), m_height(height), m_filename(filename) { m_pixels.resize(m_width * m_height); std::fill(m_pixels.begin(), m_pixels.end(), 0); // 初始化为黑色 } // 将图像元素映射到VR全景图中 void mapImage(const ImageElement& element) { for (int i = 0; i < element.getWidth(); i++) { for (int j = 0; j < element.getHeight(); j++) { int px = std::round((float)m_width * ((float)element.getX() + (float)i) / (float)m_width); int py = std::round((float)m_height * ((float)element.getY() + (float)j) / (float)m_height); m_pixels[py * m_width + px] = element.getPixel(i, j); } } } // 保存VR全景图 bool save() { Bitmap bmp(m_width, m_height, PixelFormat32bppARGB); for (int i = 0; i < m_width; i++) { for (int j = 0; j < m_height; j++) { Color color(m_pixels[j * m_width + i]); bmp.SetPixel(i, j, color); } } CLSID clsid; if (GetEncoderClsid(L"image/png", &clsid) != Ok) { return false; } bmp.Save(m_filename.c_str(), &clsid, NULL); return true; } private: int m_width, m_height; // VR全景图宽度和高度 std::string m_filename; // VR全景图文件名 std::vector<unsigned int> m_pixels; // 像素值 // 获得指定格式的编码器CLSID static int GetEncoderClsid(const WCHAR* format, CLSID* pClsid) { UINT num = 0; // 编码器数量 UINT size = 0; // 编码器数组大小 ImageCodecInfo* pImageCodecInfo = NULL; GetImageEncodersSize(&num, &size); if (size == 0) { return -1; // 没有编码器 } pImageCodecInfo = (ImageCodecInfo*)(malloc(size)); if (pImageCodecInfo == NULL) { return -1; // 内存分配错误 } GetImageEncoders(num, size, pImageCodecInfo); for (UINT j = 0; j < num; ++j) { if (wcscmp(pImageCodecInfo[j].MimeType, format) == 0) { *pClsid = pImageCodecInfo[j].Clsid; free(pImageCodecInfo); return j; // 成功 } } free(pImageCodecInfo); return -1; // 未找到 } }; int main(int argc, char* argv[]) { if (argc < 5) { std::cout << "Usage: " << argv[0] << " <image element file path> <x> <y> <VR panorama width> <VR panorama height> <VR panorama file path>" << std::endl; return -1; } // 读取图像元素并加载 std::string elementFilename = argv[1]; int elementX = std::stoi(argv[2]); int elementY = std::stoi(argv[3]); ImageElement element(elementFilename, elementX, elementY); if (!element.load()) { std::cout << "Failed to load image element: " << elementFilename << std::endl; return -1; } // 创建VR全景图 int panoramaWidth = std::stoi(argv[4]); int panoramaHeight = std::stoi(argv[5]); std::string panoramaFilename = argv[6]; VRPanorama panorama(panoramaWidth, panoramaHeight, panoramaFilename); // 将图像元素映射到VR全景图panorama.mapImage(element); // 保存VR全景图 if (!panorama.save()) { std::cout << "Failed to save VR panorama: " << panoramaFilename << std::endl; return -1; } return 0; } ``` 使用方法: ``` $ VRPanorama.exe <image element file path> <x> <y> <VR panorama width> <VR panorama height> <VR panorama file path> ``` 其中`<image element file path>`是图像元素文件路径,`<x>`和`<y>`是图像元素在VR全景图中的起始坐标,`<VR panorama width>`和`<VR panorama height>`是VR全景图的宽度和高度,`<VR panorama file path>`是VR全景图文件路径。例如: ``` $ VRPanorama.exe element.png 100 200 1024 512 panorama.png ``` 这将把`element.png`中的像素点映射到一个宽度为1024,高度为512的VR全景图中,起始坐标为`(100, 200)`,保存为`panorama.png`。
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值