大家好我是张鹏辉(道长)人如其名,我是天桥上算命的,转发这条博文,接下来一个月会有意想不到的惊喜发生。最近微博上的全景图火了,所以决定实现一下。工程里面图片资源来自网络,如有侵权请联系我,马上删除当然实现的方式很多比如OpenCV、u3d等。这里提供三种方式实现:
先看下三种实现的效果:
1.OpenGLES
第一种方式使用OpenGL来实现(上面gif图截取因为博客限制上传图片的大小,我压缩了,看起来有些卡其实很流畅的)可以看到支持旋转手机查看、或者拖动图片查看、可以看到右边中心部分有个指示器会随着角度变化而变化并且点击可以还原起始位置。
有些小伙伴懒得看原理,直接就想拿来用所以我先说集成方式吧!
在build.gradle文件中添加库依赖:
dependencies{compile'com.github.CN-ZPH:weibo360panorama:v1.0.1'}build.gradle完整代码:
publicclassMainActivityextendsAppCompatActivity{privateGLPanoramamGLPanorama;@OverrideprotectedvoidonCreate(BundlesavedInstanceState){super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);//初始化全景控件mGLPanorama=(GLPanorama)findViewById(R.id.mGLPanorama);//传入你的全景图mGLPanorama.setGLPanorama(R.drawable.imggugong);}}OK现在你可以测试玩玩了(别用模拟器玩咔咔报错就来找我)。二.分析首先我们需要了解全景图是什么东西,全景图是一种广角图。通过全景播放器可以让观看者身临其境地进入到全景图所记录的场景中去,通常标准的全景图是一张2:1的图像,其背后的实质就是等距圆柱投影。等距圆柱投影是一种将球体上的各个点投影到圆柱体的侧面上的一种投影方式,投影完之后再将它展开就是一张2:1的长方形的图像。比较常见的就是应用在地图上的投影。
得到全景图后那我们就需要展示了,看到旁边地球了吗?
怎么展示呢简单来说就是把全景图片整个贴到一个球体上。好了知道原理那我们就该考虑在android上怎么实现了,在android中绘制3d图形可以使用OpenGL(就不说OpenGL基础了想看的自己百度一大堆资料)。
纹理和图片绑定绘制到屏幕上
int[]textures=newint[1];glGenTextures(1,textures,0);inttextureId=textures[0];glBindTexture(GL_TEXTURE_2D,textureId);glTexParameterf(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_NEAREST);glTexParameterf(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);glTexParameterf(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_CLAMP_TO_EDGE);glTexParameterf(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_CLAMP_TO_EDGE);InputStreamis=context.getResources().openRawResource(drawableId);BitmapbitmapTmp;try{bitmapTmp=BitmapFactory.decodeStream(is);}finally{try{is.close();}catch(IOExceptione){e.printStackTrace();}}GLUtils.texImage2D(GL_TEXTURE_2D,0,bitmapTmp,0);bitmapTmp.recycle();2.利用传感器让球随着手机转动而转动第一想到的就是重力感应传感器,可是只能获得我们向那个位置偏移的方向,显然不可能满足我们旋转的需求,使用陀螺仪传感器。陀螺仪就是内部有一个陀螺,它的轴由于陀螺效应始终与初始方向平行,这样就可以通过与初始方向的偏差计算出实际方向。陀螺仪对设备旋转角度的检测是瞬时的而且是非常精确的。
首先注册陀螺仪传感器根据具体需要自己设置灵敏度,当然越灵敏,越耗电。
我这里为了测试设置了SENSOR_DELAY_FASTEST,实际使用建议用SENSOR_DELAY_GAME
privatevoidinitSensor(){sensorManager=(SensorManager)getSystemService(Context.SENSOR_SERVICE);gyroscopeSensor=sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE);sensorManager.registerListener(this,gyroscopeSensor,SensorManager.SENSOR_DELAY_FASTEST);}2.获得传感器数据当传感器的值发生变化时,例如磁阻传感器方向改变时会调用OnSensorChanged().当传感器的精度发生变化时会调用OnAccuracyChanged()方法。
@OverridepublicvoidonSensorChanged(SensorEventsensorEvent){if(sensorEvent.sensor.getType()==Sensor.TYPE_GYROSCOPE){if(timestamp!=0){finalfloatdT=(sensorEvent.timestamp-timestamp)*NS2S;angle[0]+=sensorEvent.values[0]*dT;angle[1]+=sensorEvent.values[1]*dT;angle[2]+=sensorEvent.values[2]*dT;floatanglex=(float)Math.toDegrees(angle[0]);floatangley=(float)Math.toDegrees(angle[1]);floatanglez=(float)Math.toDegrees(angle[2]);Sensordtinfo=newSensordt();info.setSensorX(angley);info.setSensorY(anglex);info.setSensorZ(anglez);Messagemsg=newMessage();msg.what=101;msg.obj=info;mHandler.sendMessage(msg);}timestamp=sensorEvent.timestamp;}}3.设置填充球的Y,X的角度每次获得角度数据后只需要y,x的值计算位移的值
因为全景图上下旋转会翻转整个图所以我这里设置了上下只能偏移50f,如果不限制你可以去掉
mBall.yAngle+=dx*2.0f;这里*2.0也就是陀螺仪传过来的值乘以得出偏移的角度,数值越大,每次偏移更快!
Sensordtinfo=(Sensordt)msg.obj;floaty=info.getSensorY();floatx=info.getSensorX();floatdy=y-mPreviousY;//计算触控笔Y位移floatdx=x-mPreviousX;//计算触控笔X位移mBall.yAngle+=dx*2.0f;//设置填充椭圆绕y轴旋转的角度mBall.xAngle+=dy*0.5f;//设置填充椭圆绕x轴旋转的角度if(mBall.xAngle<-50f){mBall.xAngle=-50f;}elseif(mBall.xAngle>50f){mBall.xAngle=50f;}mPreviousY=y;mPreviousX=x;3.加入手势操控,拖动图片转动加入手势这里没什么好说的了,就是重写onTouchEvent()方法。这里唯一要注意的就是,当手指点击屏幕的时候要关闭陀螺仪传感器的监听不然会引起冲突。当手指离开屏幕,重新监听陀螺仪传感器。和上面也一样只是这里换成获取手指偏移角度,而不是传感器的数值,直接看代码。
publicbooleanonTouchEvent(MotionEvente){sensorManager.unregisterListener(this);floaty=e.getY();floatx=e.getX();switch(e.getAction()){caseMotionEvent.ACTION_MOVE:floatdy=y-mPreviousYs;//计算触控笔Y位移floatdx=x-mPreviousXs;//计算触控笔X位移mBall.yAngle+=dx*0.3f;//设置填充椭圆绕y轴旋转的角度mBall.xAngle+=dy*0.3f;//设置填充椭圆绕x轴旋转的角度if(mBall.xAngle<-50f){mBall.xAngle=-50f;}elseif(mBall.xAngle>50f){mBall.xAngle=50f;}Log.i("zphsas","mHandler***mPreviousY"+mBall.yAngle);Log.i("zphsas","mHandler***mPreviousx"+mBall.xAngle);rotate();break;caseMotionEvent.ACTION_UP:sensorManager.registerListener(this,gyroscopeSensor,SensorManager.SENSOR_DELAY_FASTEST);break;}mPreviousYs=y;//记录触控笔位置mPreviousXs=x;//记录触控笔位置returntrue;}4.加入指示器指示器这里弄了一个角标指示当前在全景图的角度,并且点击还原起始角度。可以想象同样是获取角度,我们直接放在全景图改变的地方,让指示器一起改变,而我们改变的地方只有2个陀螺仪和拖动屏幕。我这里指示器放了一张图也就是一个ImageView控件
1.为指示器加入动画跟随全景图一起转
privatevoidrotate(){RotateAnimationanim=newRotateAnimation(predegrees,-mBall.yAngle,Animation.RELATIVE_TO_SELF,0.5f,Animation.RELATIVE_TO_SELF,0.5f);anim.setDuration(200);img.startAnimation(anim);predegrees=-mBall.yAngle;//记录这一次的起始角度作为下次旋转的初始角度}2.点击指示器还原起始位置
当点击还原的时候,我一开始是直接恢复起始位置可是太生硬了,通过获取当前旋转的角度,逆向旋转,慢慢还原,让其有个过渡的效果。Y轴=旋转的角度-90f(起始角度)/10f(每次偏移多少,经过我多次尝试10f在我的手机上刚刚好);得到我们总共偏移几次可以复位;X轴同理,因为我上面限制了X轴的最大偏移,这里就不就算X轴了,不过在完成的同时直接复位X轴。(只是没有过渡的效果),你可以加上。我设置的起始角度是90f和0f,也就是X,Y轴的起始点mHandlers.postDelayed(this,16);这行代码就是多少毫秒复位一次。
看代码:
privatevoidzero(){yy=(int)((mBall.yAngle-90f)/10f);mHandlers.post(newRunnable(){@Overridepublicvoidrun(){if(yy!=0){if(yy>0){mBall.yAngle=mBall.yAngle-10f;mHandlers.postDelayed(this,16);yy--;}if(yy<0){mBall.yAngle=mBall.yAngle+10f;mHandlers.postDelayed(this,16);yy++;}}else{mBall.yAngle=90f;}mBall.xAngle=0f;}});}至此第一种OpenGLES方式核心代码分析完毕,有不明白的地方可以找我第二种GoogleVR第二种也就是谷歌官方为移动平台下VR解决方案,有兴趣的可以点开下面链接玩玩,我们只使用其中全景图模块。
目前GitHub上最新版本号为1.8.0,我这里也用最新的了。最低支持到minSdkVersion19也就是Android4.4.0在build.gradle文件中添加库依赖:
考虑到在多种机型兼容性,还有原生WebView的一些坑,我这里使用腾讯的X5内核的WebView。
到x5官网下载最新的sdk得到一个jar包我在这的是3.3.0版本的。将下载好的jar包放到你的工程libs目录下在build.gradle文件中添加库依赖:
你可以使用js交互将你的地址传到HTML上
直接上代码了:
publicclassWebViewActivityextendsAppCompatActivity{privatecom.tencent.smtt.sdk.WebViewtencent_webview;privateStringurl="file:///android_asset/admin.html";@OverrideprotectedvoidonCreate(BundlesavedInstanceState){super.onCreate(savedInstanceState);setContentView(R.layout.activity_web_view);initView();}@SuppressLint("SetJavaScriptEnabled")privatevoidinitView(){tencent_webview=(WebView)findViewById(R.id.web);tencent_webview.loadUrl(url);WebSettingswebSettings=tencent_webview.getSettings();webSettings.setJavaScriptEnabled(true);tencent_webview.setWebViewClient(newWebViewClient(){@OverridepublicbooleanshouldOverrideUrlLoading(WebViewview,Stringurl){returntrue;}});}}