简介
大家好我是张鹏辉(道长)人如其名,我是天桥上算命的,转发这条博文,接下来一个月会有意想不到的惊喜发生。
最近微博上的全景图火了,所以决定实现一下。
工程里面图片资源来自网络,如有侵权请联系我,马上删除
当然实现的方式很多比如OpenCV、u3d等。
这里提供三种方式实现:
1. OpenGL ES
2. GoogleCardboard(Google VR)上面的一个集成模块,我们只使用里面展示全景图部分模块
3. Three.js(利用前端姿势)WebView混合开发
三种姿势孰强孰弱,根据需求你们自己判断!我会在结尾给出一些建议,多说无益开撸
先看下三种实现的效果:
1.OpenGL ES
2.Google VR(全景图模块)
3.Three.js(利用前端姿势)WebView混合开发
第一种方式使用OpenGL来实现(上面gif图截取因为博客限制上传图片的大小,我压缩了,看起来有些卡其实很流畅的)
可以看到支持旋转手机查看、或者拖动图片查看、可以看到右边中心部分有个指示器会随着角度变化而变化并且点击可以还原起始位置。
有些小伙伴懒得看原理,直接就想拿来用所以我先说集成方式吧!
Step 1
在build.gradle 文件中添加库依赖:
allprojects {
repositories {
maven { url 'https://jitpack.io' }
}
}
Step 2.Add the dependency
在 build.gradle 文件中添加库依赖:
dependencies {
compile 'com.github.CN-ZPH:weibo360panorama:v1.0.1'
}
build.gradle 完整代码:
apply plugin: 'com.android.application'
android {
compileSdkVersion 26
buildToolsVersion "26.0.1"
defaultConfig {
applicationId "com.zph.three360panorama"
minSdkVersion 19
targetSdkVersion 26
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
allprojects {
repositories {
maven { url 'https://jitpack.io' }
}
}
}
dependencies {
compile fileTree(include: ['*.jar'], dir: 'libs')
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.android.support:appcompat-v7:26.+'
compile 'com.android.support.constraint:constraint-layout:1.0.2'
compile 'com.android.support:design:26.+'
compile 'com.github.CN-ZPH:weibo360panorama:v1.0.1'
compile 'com.google.vr:sdk-panowidget:1.80.0'
testCompile 'junit:junit:4.12'
compile files('libs/tbs_sdk_thirdapp_v3.3.0.1045_43300.jar')
}
Step 3.创建布局文件.XML
<com.zph.glpanorama.GLPanorama
android:id="@+id/mGLPanorama"
android:layout_width="match_parent"
android:layout_height="match_parent">com.zph.glpanorama.GLPanorama>
Step 4.传入你的全景图
R.drawable.imggugong 这张全景图传到控件里面
public class MainActivity extends AppCompatActivity {
private GLPanorama mGLPanorama;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//初始化全景控件
mGLPanorama= (GLPanorama) findViewById(R.id.mGLPanorama);
//传入你的全景图
mGLPanorama.setGLPanorama(R.drawable.imggugong);
}
}
首先我们需要了解全景图是什么东西,全景图是一种广角图。通过全景播放器可以让观看者身临其境地进入到全景图所记录的场景中去,通常标准的全景图是一张2:1的图像,其背后的实质就是等距圆柱投影。等距圆柱投影是一种将球体上的各个点投影到圆柱体的侧面上的一种投影方式,投影完之后再将它展开就是一张2:1的长方形的图像。比较常见的就是应用在地图上的投影。
得到全景图后那我们就需要展示了,看到旁边地球了吗?
怎么展示呢简单来说就是把全景图片整个贴到一个球体上。
好了知道原理那我们就该考虑在android上怎么实现了,在android中绘制3d图形可以使用OpenGL (就不说OpenGL 基础了想看的自己百度一大堆资料)。
引用tim_shadow大佬的关于全景图一篇文章介绍
在OpenGL ES中基本上所有的立体图像都是通过一个个的小三角形拼接而成我们知道球面上面的每一个点(P(x,y,z))都会满足方程组(球的极坐标方程):
x = r * sin(a) *cos(b)
y = r * cos(a)
z = r * sin(a)*sin(b)
其中 r为球的半径,a为线段 OP与 z轴正方向所夹角,b为 OP在xoy平面的投影 OP‘ 与x的正方向所夹角
我们可以根据这个方程组,通过控制∠a和∠b的变化,从上到下,逆时针的取得我们需要用来组合称三角形的点,然后我们需要将全景图片上的点与我们在球上面选取的点一一对应起来(注意:球的坐标是3维坐标,图片的坐标是2维坐标)
球上面的点与图片上面的点一一对应起来。
纹理和图片绑定绘制到屏幕上
int[] textures = new int[1];
glGenTextures(1, textures, 0);
int textureId = 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);
InputStream is = context.getResources().openRawResource(drawableId);
Bitmap bitmapTmp;
try {
bitmapTmp = BitmapFactory.decodeStream(is);
} finally {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
GLUtils.texImage2D(GL_TEXTURE_2D, 0, bitmapTmp, 0);
bitmapTmp.recycle();
第一想到的就是重力感应传感器,可是只能获得我们向那个位置偏移的方向,显然不可能满足我们旋转的需求,使用陀螺仪传感器。
陀螺仪就是内部有一个陀螺,它的轴由于陀螺效应始终与初始方向平行,这样就可以通过与初始方向的偏差计算出实际方向。
陀螺仪对设备旋转角度的检测是瞬时的而且是非常精确的。
首先注册陀螺仪传感器根据具体需要自己设置灵敏度,当然越灵敏,越耗电。
- 注册陀螺仪传感器,并设定传感器向应用中输出的时间间隔类型是SensorManager.SENSOR_DELAY_GAME(20000微秒)
- SensorManager.SENSOR_DELAY_FASTEST(0微秒):最快。最低延迟,一般不是特别敏感的处理不推荐使用,该模式可能在成手机电力大量消耗,由于传递的为原始数据,算法不处理好会影响游戏逻辑和UI的性能
- SensorManager.SENSOR_DELAY_GAME(20000微秒):游戏。游戏延迟,一般绝大多数的实时性较高的游戏都是用该级别
- SensorManager.SENSOR_DELAY_NORMAL(200000微秒):普通。标准延时,对于一般的益智类或EASY级别的游戏可以使用,但过低的采样率可能对一些赛车类游戏有跳帧现象
- SensorManager.SENSOR_DELAY_UI(60000微秒):用户界面。一般对于屏幕方向自动旋转使用,相对节省电能和逻辑处理,一般游戏开发中不使用
我这里为了测试设置了SENSOR_DELAY_FASTEST,实际使用建议用SENSOR_DELAY_GAME
private void initSensor() {
sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
gyroscopeSensor = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE);
sensorManager.registerListener(this, gyroscopeSensor,
SensorManager.SENSOR_DELAY_FASTEST);
}
当传感器的值发生变化时,例如磁阻传感器方向改变时会调用OnSensorChanged(). 当传感器的精度发生变化时会调用OnAccuracyChanged()方法。
从 x、y、z 轴的正向位置观看处于原始方位的设备,如果设备逆时针旋转,将会收到正值;否则,为负值
得到两次检测到手机旋转的时间差(纳秒),并将其转化为秒
将手机在各个轴上的旋转角度相加,即可得到当前位置相对于初始位置的旋转弧度,将弧度转化为角度
@Override
public void onSensorChanged(SensorEvent sensorEvent) {
if (sensorEvent.sensor.getType() == Sensor.TYPE_GYROSCOPE) {
if (timestamp != 0) {
final float dT = (sensorEvent.timestamp - timestamp) * NS2S;
angle[0] += sensorEvent.values[0] * dT;
angle[1] += sensorEvent.values[1] * dT;
angle[2] += sensorEvent.values[2] * dT;
float anglex = (float) Math.toDegrees(angle[0]);
float angley = (float) Math.toDegrees(angle[1]);
float anglez = (float) Math.toDegrees(angle[2]);
Sensordt info = new Sensordt();
info.setSensorX(angley);
info.setSensorY(anglex);
info.setSensorZ(anglez);
Message msg = new Message();
msg.what = 101;
msg.obj = info;
mHandler.sendMessage(msg);
}
timestamp = sensorEvent.timestamp;
}
}
每次获得角度数据后只需要y,x的值计算位移的值
因为全景图上下旋转会翻转整个图所以我这里设置了上下只能偏移50f,如果不限制你可以去掉
mBall.yAngle += dx * 2.0f;这里*2.0也就是陀螺仪传过来的值乘以得出偏移的角度,数值越大,每次偏移更快!
Sensordt info = (Sensordt) msg.obj;
float y = info.getSensorY();
float x = info.getSensorX();
float dy = y - mPreviousY;// 计算触控笔Y位移
float dx = x - mPreviousX;// 计算触控笔X位移
mBall.yAngle += dx * 2.0f;// 设置填充椭圆绕y轴旋转的角度
mBall.xAngle += dy * 0.5f;// 设置填充椭圆绕x轴旋转的角度
if (mBall.xAngle < -50f) {
mBall.xAngle = -50f;
} else if (mBall.xAngle > 50f) {
mBall.xAngle = 50f;
}
mPreviousY = y;
mPreviousX = x;
加入手势这里没什么好说的了,就是重写onTouchEvent()方法。
这里唯一要注意的就是,当手指点击屏幕的时候要关闭陀螺仪传感器的监听不然会引起冲突。当手指离开屏幕,重新监听陀螺仪传感器。
和上面也一样只是这里换成获取手指偏移角度,而不是传感器的数值,直接看代码。
public boolean onTouchEvent(MotionEvent e) {
sensorManager.unregisterListener(this);
float y = e.getY();
float x = e.getX();
switch (e.getAction()) {
case MotionEvent.ACTION_MOVE:
float dy = y - mPreviousYs;// 计算触控笔Y位移
float dx = x - mPreviousXs;// 计算触控笔X位移
mBall.yAngle += dx * 0.3f;// 设置填充椭圆绕y轴旋转的角度
mBall.xAngle += dy * 0.3f;// 设置填充椭圆绕x轴旋转的角度
if (mBall.xAngle < -50f) {
mBall.xAngle = -50f;
} else if (mBall.xAngle > 50f) {
mBall.xAngle = 50f;
}
Log.i("zphsas", "mHandler *** mPreviousY" + mBall.yAngle);
Log.i("zphsas", "mHandler *** mPreviousx" + mBall.xAngle);
rotate();
break;
case MotionEvent.ACTION_UP:
sensorManager.registerListener(this, gyroscopeSensor,
SensorManager.SENSOR_DELAY_FASTEST);
break;
}
mPreviousYs = y;// 记录触控笔位置
mPreviousXs = x;// 记录触控笔位置
return true;
}
指示器这里弄了一个角标指示当前在全景图的角度,并且点击还原起始角度。
可以想象同样是获取角度,我们直接放在全景图改变的地方,让指示器一起改变,而我们改变的地方只有2个陀螺仪和拖动屏幕。
我这里指示器放了一张图也就是一个 ImageView 控件
1.为指示器加入动画跟随全景图一起转
private void rotate() {
RotateAnimation anim = new RotateAnimation(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);
这行代码就是多少毫秒复位一次。
看代码:
private void zero() {
yy = (int) ((mBall.yAngle - 90f) / 10f);
mHandlers.post(new Runnable() {
@Override
public void run() {
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;
}
});
}
第二种也就是谷歌官方为移动平台下VR解决方案,有兴趣的可以点开下面链接玩玩,我们只使用其中全景图模块。
Google VR主页:https://developers.google.com/vr/
Google VR for Android github地址:https://github.com/googlevr/gvr-android-sdk
Step 1.Add the dependency
目前GitHub上最新版本号为1.8.0,我这里也用最新的了。
最低支持到 minSdkVersion 19 也就是Android 4.4.0
在 build.gradle 文件中添加库依赖:
dependencies {
compile 'com.google.vr:sdk-panowidget:1.80.0'
}
Step 2.创建布局文件.XML
<com.google.vr.sdk.widgets.pano.VrPanoramaView
android:id="@+id/mVrPanoramaView"
android:layout_width="match_parent"
android:layout_height="250dip"/>
Step 3.AndroidManifest中添加权限
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
//因为全景图较大,所以在application下申请更多空间,但是作为一个有节操的码农建议你不要这么干。
<application android:largeHeap="true" application>
Step 4.Activity中初始化组件
//初始化VR图片
private void initVrPaNormalView() {
mVrPanoramaView = (VrPanoramaView) findViewById(R.id.mVrPanoramaView);
paNormalOptions = new VrPanoramaView.Options();
paNormalOptions.inputType = VrPanoramaView.Options.TYPE_STEREO_OVER_UNDER;
// mVrPanoramaView.setFullscreenButtonEnabled (false); //隐藏全屏模式按钮
mVrPanoramaView.setInfoButtonEnabled(false); //设置隐藏最左边信息的按钮
mVrPanoramaView.setStereoModeButtonEnabled(false); //设置隐藏立体模型的按钮
mVrPanoramaView.setEventListener(new ActivityEventListener()); //设置监听
//加载本地的图片源
mVrPanoramaView.loadImageFromBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.andes), paNormalOptions);
//设置网络图片源
// panoWidgetView.loadImageFromByteArray();
}
private class ActivityEventListener extends VrPanoramaEventListener {
@Override
public void onLoadSuccess() {//图片加载成功
}
@Override
public void onLoadError(String errorMessage) {//图片加载失败
}
@Override
public void onClick() {//当我们点击了VrPanoramaView 时候触发 super.onClick();
}
@Override
public void onDisplayModeChanged(int newDisplayMode) {
super.onDisplayModeChanged(newDisplayMode);
}
}
Three.js是JavaScript编写的WebGL第三方库。提供了非常多的3D显示功能。
Android下相信很多人都多少做过前端开发,现在很多应用程序都是基于前端H5/RN/小程序等来玩的。
当然我们全景图也可以放到前端来实现,套个WebView利用JavaScript与Android交互来实现一部分功能。
考虑到在多种机型兼容性,还有原生WebView的一些坑,我这里使用腾讯的X5内核的WebView。
Step 1.添加x5 SDK
到x5官网下载最新的sdk得到一个jar包
我在这的是3.3.0版本的。
将下载好的jar包放到你的工程libs目录下
在 build.gradle 文件中添加库依赖:
dependencies {
compile files('libs/tbs_sdk_thirdapp_v3.3.0.1045_43300_sharewithdownload_withoutGame_obfs_20170605_170212.jar')
}
Step 2.AndroidManifest.xml里加入权限声明
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
Step 3.APPAplication中X5内核初始化
public class APPAplication extends Application {
@Override
public void onCreate() {
// TODO Auto-generated method stub
super.onCreate();
//搜集本地tbs内核信息并上报服务器,服务器返回结果决定使用哪个内核。
QbSdk.PreInitCallback cb = new QbSdk.PreInitCallback() {
@Override
public void onViewInitFinished(boolean arg0) {
// TODO Auto-generated method stub
//x5內核初始化完成的回调,为true表示x5内核加载成功,否则表示x5内核加载失败,会自动切换到系统内核。
}
@Override
public void onCoreInitFinished() {
// TODO Auto-generated method stub
}
};
//x5内核初始化接口
QbSdk.initX5Environment(getApplicationContext(), cb);
}
}
Step 4..创建布局文件.XML
<com.tencent.smtt.sdk.WebView
android:id="@+id/web"
android:layout_width="match_parent"
android:layout_height="match_parent">com.tencent.smtt.sdk.WebView>
Step 5.下载Three.js
下载地址:https://threejs.org/
或者去GitHub从我的项目中找今天代码都会放到GitHub上
<script src="js/three.min.js">script>
<script src="js/photo-sphere-viewer.min.js">script>
Step 6.编写HTML文件
在 assets 目录下创建一个html文件展示全景图
引入Threejs
panorama:’https://gw.alicdn.com/tfs/TB1WSInRFXXXXXlXpXXXXXXXXXX-1200-600.jpg‘, 这行就是你的全景图地址
你可以使用js交互将你的地址传到HTML上
直接上代码了:
<html>
<head>
<meta charset="utf-8" />
<title>Photo Sphere Viewertitle>
<meta name="viewport" content="initial-scale=1.0" />
<script src="js/three.min.js">script>
<script src="js/photo-sphere-viewer.min.js">script>
<style>
html, body {
margin: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
#container {
width: 100%;
height: 100%;
}
style>
head>
<body>
<div id="container">div>
<script>
var div = document.getElementById('container');
var PSV = new PhotoSphereViewer({
panorama: 'https://gw.alicdn.com/tfs/TB1WSInRFXXXXXlXpXXXXXXXXXX-1200-600.jpg',
container: div,
time_anim: false,
navbar: true,
navbar_style: {
backgroundColor: 'rgba(58, 67, 77, 0.7)'
},
});
script>
body>
html>
Step 7.Activity调用HTML
很简单就是把系统的WebView换成Tencent_Webview其他类似
public class WebViewActivity extends AppCompatActivity {
private com.tencent.smtt.sdk.WebView tencent_webview;
private String url = "file:///android_asset/admin.html";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_web_view);
initView();
}
@SuppressLint("SetJavaScriptEnabled")
private void initView() {
tencent_webview = (WebView) findViewById(R.id.web);
tencent_webview.loadUrl(url);
WebSettings webSettings = tencent_webview.getSettings();
webSettings.setJavaScriptEnabled(true);
tencent_webview.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
return true;
}
});
}
}
最后附上插件的可配置参数:
panorama:必填参数,全景图的路径。
container:必填参数,放置全景图的div元素。
autoload:可选,默认值为true,true为自动调用全景图,false为在后面加载全景图(通过.load()方法)。
usexmpdata:可选,默认值为true,如果Photo Sphere Viewer必须读入XMP数据则为true。
default_position:可选,默认值为{},定义默认的位置,及用户看见的第一个点,例如:{long: Math.PI, lat: Math.PI/2}。
min_fov:可选,默认值为30,观察的最小区域,单位degrees,在1-179之间。
max_fov:可选,默认值为90,观察的最大区域,单位degrees,在1-179之间。
allow_user_interactions:可选,默认值为true,设置为false则禁止用户和全景图交互(导航条不可用)。
tilt_up_max:可选,默认值为Math.PI/2,向上倾斜的最大角度,单位radians。
tilt_down_max:可选,默认值为Math.PI/2,向下倾斜的最大角度,单位radians。
zoom_level:可选,默认值为0,默认的缩放级别,值在0-100之间。
long_offset:可选,默认值为PI/360,mouse/touch移动时每像素经过的经度值。
lat_offset:可选,默认值为PI/180,mouse/touch移动时每像素经过的纬度值。
time_anim:可选,默认值为2000,全景图在time_anim毫秒后会自动进行动画。(设置为false禁用它)
theta_offset:过时的选项,可选,默认值为1440,自动动画时水平方向的速度。
anim_speed:可选,默认值为2rpm,动画的速度,每秒/分钟多少radians/degrees/revolutions。
navbar:可选值,默认为false。显示导航条。
navbar_style:可选值,默认为{}。导航条的自定义样式。下面是可用的样式列表:
backgroundColor:导航条的背景颜色,默认值为rgba(61, 61, 61, 0.5)。
buttonsColor:按钮的前景颜色,默认值为transparent。
activeButtonsBackgroundColor:按钮激活状态的背景颜色,默认值为rgba(255, 255, 255, 0.1)。
buttonsHeight:按钮的高度,单位像素,默认值为20。
autorotateThickness:autorotate图标的厚度,单位像素,默认值为1。
zoomRangeWidth:缩放的范围,单位显示,默认值50。
zoomRangeThickness:缩放的范围的厚度,单位像素,默认值1。
zoomRangeDisk:缩放范围的圆盘直径,单位像素,默认值为7。
fullscreenRatio:全屏图标的比例,默认值为3/4。
fullscreenThickness:全屏图标的厚度,单位像素,默认值为2。
loading_msg:可选,默认值为Loading…,图片加载时的提示文字。
loading_img:可选,默认值为null,在加载时显示的图片的路径。
size:可选,默认值null,全景图容器的最终尺寸。例如:{width: 500, height: 300}。
onready:可选值,默认值为null。当全景图准备就绪并且第一张图片显示时的回调函数。
三种方式都实现完了,不用担心今天所有代码都会放在GitHub上。
三种方式具体你使用哪种我还是没有推荐的
这里只是一张图,你可以多张图实现来完成简单的全景街景功能!点击图片某个区域,跳转到下一个街景的图,包括百度地图里面也是一张张全景图拼接而成。
- 第一种我会在后续继续完善加入更多的可选参数,你们有兴趣也可以自己优化。
- 第二种是谷歌VR模块的没什么好说的,毕竟官方俩字就够了。
- 第三种跨平台最好的,毕竟是个网页。而我们第三种使用了腾讯X5内核来玩,但是还可以在优化,消耗不小,我建议你单独给WebView分配一个进程和你的业务分离。
- 拿着我的保温杯,泡一杯枸杞,我们下篇文章再会
https://github.com/CN-ZPH/
觉得不错请点一个star蛤!
有问题下面留言评论,我看到会回复。
或加我QQ:1344670918。