2017年安卓巴士全球開發者論壇-武漢站
我有幾句心裡話 想對在武漢工作的開發者說···
距離活動開始還有兩天,武漢的開發者們趕快報名行動起來吧!
簡介
大家好我是張鵬輝(道長)人如其名,我是天橋上算命的,轉發這條博文,接下來一個月會有意想不到的驚喜發生。
最近微博上的全景圖火了,所以決定實現一下。
工程裡面圖片資源來自網絡,如有侵權請聯繫我,馬上刪除。當然實現的方式很多比如OpenCV、u3d等。
這裡提供三種方式實現:
OpenGL ES
GoogleCardboard(Google VR)上面的一個集成模塊,我們只使用裡面展示全景圖部分模塊
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' } } }
在 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')}
<com.zph.glpanorama.GLPanorama android:id="@+id/mGLPanorama" android:layout_width="match_parent" android:layout_height="match_parent">
</com.zph.glpanorama.GLPanorama>
*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 基礎了想看的自己百度一大堆資料)。
1.繪製球體:引用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; float dx = x - mPreviousX; mBall.yAngle += dx * 2.0f; mBall.xAngle += dy * 0.5f; 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; float dx = x - mPreviousXs; mBall.yAngle += dx * 0.3f; mBall.xAngle += dy * 0.3f; 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
目前GitHub上最新版本號為1.8.0,我這裡也用最新的了。最低支持到 minSdkVersion 19 也就是Android 4.4.0。
在 build.gradle 文件中添加庫依賴:
dependencies { compile 'com.google.vr:sdk-panowidget:1.80.0' }
<com.google.vr.sdk.widgets.pano.VrPanoramaView android:id="@+id/mVrPanoramaView" android:layout_width="match_parent" android:layout_height="250dip"/>
<!-- These permissions are used by Google VR SDK to get the best Google VR headset profiles. !--> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <application android:largeHeap="true" </application>
private void initVrPaNormalView() { mVrPanoramaView = (VrPanoramaView) findViewById(R.id.mVrPanoramaView); paNormalOptions = new VrPanoramaView.Options(); paNormalOptions.inputType = VrPanoramaView.Options.TYPE_STEREO_OVER_UNDER;
mVrPanoramaView.setInfoButtonEnabled(false); mVrPanoramaView.setStereoModeButtonEnabled(false); mVrPanoramaView.setEventListener(new ActivityEventListener()); mVrPanoramaView.loadImageFromBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.andes), paNormalOptions);
}
private class ActivityEventListener extends VrPanoramaEventListener {
@Override public void onLoadSuccess() { }
@Override public void onLoadError(String errorMessage) { }
@Override public void 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') }
<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" />
public class APPAplication extends Application {
@Override public void onCreate() {
super.onCreate();
QbSdk.PreInitCallback cb = new QbSdk.PreInitCallback() { @Override public void onViewInitFinished(boolean arg0) {
} @Override public void onCoreInitFinished() {
} }; QbSdk.initX5Environment(getApplicationContext(), cb); }}
<com.tencent.smtt.sdk.WebView android:id="@+id/web" android:layout_width="match_parent" android:layout_height="match_parent">
</com.tencent.smtt.sdk.WebView>
下載地址:https://threejs.org/
或者去GitHub從我的項目中找今天代碼都會放到GitHub上
<script src="js/three.min.js"></script> <script src="js/photo-sphere-viewer.min.js"></script>
在 assets 目錄下創建一個html文件展示全景圖引入Threejs
panorama:'https://gw.alicdn.com/tfs/TB1WSInRFXXXXXlXpXXXXXXXXXX-1200-600.jpg', 這行就是你的全景圖地址
你可以使用js交互將你的地址傳到HTML上
直接上代碼了:
<!DOCTYPE html><html> <head> <meta charset="utf-8" /> <title>Photo Sphere Viewer</title> <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>
很簡單就是把系統的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分配一個進程和你的業務分離。
拿著我的保溫杯,泡一杯枸杞,我們下篇文章再會
源碼地址:
http://www.apkbus.com/thread-462911-1-1.html
大家都在看
Android 對比MVC、MVP來聊聊MVVM模式的理解
移動端WebView的使用和JavaScript交互
這裡有十個Android的另類庫,你都知道麼
Android屏幕適配的那些事兒
好文章來之不易,分享至朋友圈讓更多朋友受益
歡迎巴友留言,一起探討成長