Mapbox樣貌
Mapbox地圖相當漂亮, 跟Google不相上下, 見下圖
不再使用Google Map
GoogleMap確實有其好用的地方. 但裏面的圖資, 尤其是經緯度轉地址, 卻是由廣大的網民所提供的. 許多景點的照片, 也是由網民自發上傳
但自2018年開始, Google竟然開始向程式開發人員收取費用, 且依查詢次數作為收費標準. 網路上就有人發表, 一個小小的個人工作室, 一個月竟被Google收取了近八萬元台幣, 且因必需綁定信用卡付費, 當帳單寄來, 已經來不及止付了。
為了抵制這種無恥的行為, 本人決定拋磚引玉, 撰寫此文, 鼓勵大家改用完全免費的MapBox, 將Google趕出台灣.
Mapbox 版本差異
6.5.0 :
此版為本人第一次使用的版本
7.2.0:
在onMapReady需加入 mapboxMap.setStyle, 否則無地圖顯示. 此版使用Android 3.5時, 執行Mapbox.getInstance()即會有閃退的現象, 此為Mapbox的bug
8.4.0 :
此版需使用LocationEngine.requestLocationUpdates() 設定LocationEngineCallback, 這樣地圖才能隨GPS變更而移動. 若使用 7.2.0的寫法, 地圖是不會動的。
底下代碼使用8.4.0開發, 所以地圖的移動沒有問題
GPS模擬器
開發測試地圖時, 常需要走來走去, 實在是麻煩, 所以可以到Goolge Play安裝 GPS Joystick 模擬器, 就可以隨時改便GPS的訊號了
MapBox SDK
mapbox的設計相當便利, 不需手動下載安裝什麼 jar之類的sdk, 只需在build.gradle(Module:app) 裏 :
1. 加入 repositories
repositories {
mavenCentral()
}
2. dependencies加入
dependencies {
implementation 'com.mapbox.mapboxsdk:mapbox-android-sdk:8.4.0'
}
然後按下sync, 就會自動下載sdk了, 然後就可以直接引用MapView類別
最新版本資訊請參考
https://docs.mapbox.com/android/maps/overview/
gradle的完整設定檔如下
apply plugin: 'com.android.application' android { compileSdkVersion 28 defaultConfig { applicationId "com.asuscomm.mahaljsp.mapboxtest2" minSdkVersion 21 targetSdkVersion 28 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } compileOptions { sourceCompatibility = '1.8' targetCompatibility = '1.8' } } repositories { mavenCentral() } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'androidx.appcompat:appcompat:1.0.2' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' implementation 'com.mapbox.mapboxsdk:mapbox-android-sdk:8.4.0' }
註冊及申請憑証
請到如下網站申請一個帳號
https://account.mapbox.com/auth/signup/
然後到 https://account.mapbox.com 進行登入, 於Access tokens按下Create a token, 然後就會取得一個憑証, 記錄下來等會再於代碼中貼上.
AndroidManifest.xml
Mapbox會使用到網路及GPS精準定位, 所以請於AndroidManifest.xml加入如下權限. 另精準定位屬於危險請求, Android 6.0以上需再撰寫請求權限代碼
完整代碼如下
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.asuscomm.mahaljsp.mapboxtest2"> <uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.AppCompat.Light.NoActionBar"> <activity android:name=".MainActivity" android:screenOrientation="landscape"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
Layout
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal" tools:context=".MainActivity"> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:layout_weight="6" android:background="#550000ff"></LinearLayout> <LinearLayout android:layout_weight="1" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <RelativeLayout android:layout_weight="1" android:id="@+id/container" android:layout_width="match_parent" android:layout_height="match_parent"> </RelativeLayout> <LinearLayout android:background="#55ffff00" android:layout_weight="5" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/txt" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="18sp" /> </LinearLayout> </LinearLayout> </LinearLayout>
建立自訂MapView
此處我們新增一個自訂的類別, 可供日後直接使用, 也方便日後匯出成jar sdk檔案. 首先新增一個類別檔, 繼承MapView並實作onMapReadyCallback
enableLocationComponent()方法, 打開目前位置的圖標顯示, 打開後地圖會隨著GPS的變動而移動及旋轉, 這種追蹤功能完全自動, 我們不需顧及地圖的操作.
OnMapClickListener 可讓使用者在地圖上點一下, 重設拉回裝置所在位置.
另外, 當經緯度改變時, 我們必需把經緯度傳回MainActivity進行顯示.
6.5.0 使用OnLocationChangedListener的監聽器.
7.2.0 使用OnCameraMoveListener 監聽
8.4.0 使用LocationEngine.requestLocationUpdates() 監聽
package com.asuscomm.thomasmap; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.location.Location; import android.os.Bundle; import android.os.Handler; import android.os.Message; import androidx.annotation.NonNull; import com.mapbox.android.core.location.LocationEngine; import com.mapbox.android.core.location.LocationEngineCallback; import com.mapbox.android.core.location.LocationEngineProvider; import com.mapbox.android.core.location.LocationEngineRequest; import com.mapbox.android.core.location.LocationEngineResult; import com.mapbox.mapboxsdk.camera.CameraPosition; import com.mapbox.mapboxsdk.camera.CameraUpdateFactory; import com.mapbox.mapboxsdk.geometry.LatLng; import com.mapbox.mapboxsdk.location.LocationComponent; import com.mapbox.mapboxsdk.location.LocationComponentActivationOptions; import com.mapbox.mapboxsdk.location.LocationComponentOptions; import com.mapbox.mapboxsdk.location.modes.CameraMode; import com.mapbox.mapboxsdk.location.modes.RenderMode; import com.mapbox.mapboxsdk.maps.MapView; import com.mapbox.mapboxsdk.maps.MapboxMap; import com.mapbox.mapboxsdk.maps.OnMapReadyCallback; import com.mapbox.mapboxsdk.maps.Style; import static android.os.Looper.getMainLooper; public class MahalMap extends MapView implements OnMapReadyCallback { Handler handler; private static MapboxMap mapboxMap; private static double mapLat, mapLng; Context context; LocationComponent locationComponent; Location location; int locationMark; LocationEngine locationEngine; boolean firstFlag=true; private long DEFAULT_INTERVAL_IN_MILLISECONDS = 1000L; private long DEFAULT_MAX_WAIT_TIME = DEFAULT_INTERVAL_IN_MILLISECONDS * 5; public MahalMap(@NonNull Context context, Handler handler, int locationMark) { super(context); this.context = context; this.handler = handler; this.locationMark=locationMark; getMapAsync(this); } @Override public void onMapReady(@NonNull MapboxMap mapboxMap) { this.mapboxMap = mapboxMap; mapboxMap.setStyle(Style.MAPBOX_STREETS, new Style.OnStyleLoaded() { @Override public void onStyleLoaded(@NonNull Style style) { enableLocationComponent(); } }); } @SuppressLint("MissingPermission") private void enableLocationComponent() { locationComponent = mapboxMap.getLocationComponent(); Bitmap bitmap= BitmapFactory.decodeResource(context.getResources(), locationMark); LocationComponentOptions options=LocationComponentOptions.builder(context) .layerBelow("mark") .gpsDrawable(locationMark) .bearingTintColor(0xff0000) .accuracyAlpha(0.0f) .padding(new int[]{0,0,0,bitmap.getHeight()}) .build(); LocationComponentActivationOptions actOptions = LocationComponentActivationOptions.builder(context, mapboxMap.getStyle()) .useDefaultLocationEngine(true) .locationComponentOptions(options) .build(); locationComponent.activateLocationComponent(actOptions); locationComponent.setCameraMode(CameraMode.TRACKING_GPS); locationComponent.setRenderMode(RenderMode.GPS); locationEngine = LocationEngineProvider.getBestLocationEngine(context); LocationEngineRequest request = new LocationEngineRequest.Builder(DEFAULT_INTERVAL_IN_MILLISECONDS) .setPriority(LocationEngineRequest.PRIORITY_HIGH_ACCURACY) .setMaxWaitTime(DEFAULT_MAX_WAIT_TIME).build(); locationEngine.requestLocationUpdates(request,locationUpdate,getMainLooper()); locationComponent.setLocationEngine(locationEngine); mapboxMap.addOnMapClickListener(clickListener); locationComponent.setLocationComponentEnabled(true); resetMap(); } private MapboxMap.OnMapClickListener clickListener=new MapboxMap.OnMapClickListener() { @Override public boolean onMapClick(@NonNull LatLng point) { resetMap(); return false; } }; private void resetMap(){ location=locationComponent.getLastKnownLocation(); if (location != null) { mapLat = location.getLatitude(); mapLng = location.getLongitude(); CameraPosition position = new CameraPosition.Builder() .target(new LatLng(location.getLatitude(), location.getLongitude()))//Sets camera position .zoom(16) //Sets the zoom .tilt(0) //Set the camera tilt .bearing(location.getBearing())//bearing Map .build(); //Creates a CameraPosition from the builder mapboxMap.animateCamera(CameraUpdateFactory.newCameraPosition(position),10); firstFlag=false; } } private LocationEngineCallback<LocationEngineResult> locationUpdate=new LocationEngineCallback<LocationEngineResult>() { @Override public void onSuccess(LocationEngineResult result) { location=locationComponent.getLastKnownLocation(); if(location!=null) { mapLat = location.getLatitude(); mapLng = location.getLongitude(); CameraPosition.Builder builder=new CameraPosition.Builder(); builder.target(new LatLng(mapLat, mapLng))//Sets camera position .bearing(location.getBearing()) .tilt(0) //Set the camera tilt .build(); //Creates a CameraPosition from the builder if (firstFlag){ firstFlag=false; builder.zoom(16); } mapboxMap.animateCamera(CameraUpdateFactory.newCameraPosition(builder.build()), 1000); Bundle bundle = new Bundle(); bundle.putDouble("LAT", location.getLatitude()); bundle.putDouble("LNG", location.getLongitude()); Message msg=new Message(); msg.what=1; msg.setData(bundle); handler.sendMessage(msg); } } @Override public void onFailure(@NonNull Exception exception) {} }; @Override public void onDestroy() { if (locationEngine != null) { locationEngine.removeLocationUpdates(locationUpdate); } super.onDestroy(); } }
Activity的使用方法
在Activity中, 要使用我們自訂的MapView, 需依如下步驟
1. 要求權限
2. Mapbox.getInstance(this, token); 此處跟6.5.0不一樣, 在7.2.0必需先getInstance登記憑証後, 才能new 出 MapView
3. new出MapView後, 加入Layout中
完整代碼如下
package com.asuscomm.thomasmap; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import android.Manifest; import android.content.pm.PackageManager; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.view.WindowManager; import android.widget.RelativeLayout; import android.widget.TextView; import android.widget.Toast; import com.mapbox.mapboxsdk.Mapbox; public class MainActivity extends AppCompatActivity { MahalMap map; RelativeLayout container; TextView txt; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getWindow().setFlags( WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN ); setContentView(R.layout.activity_main); getPermission(); } private void getPermission(){ if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.M){ int hasPermission=checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION); if(hasPermission!= PackageManager.PERMISSION_GRANTED){ requestPermissions( new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, 100); } else{ init(); } } else{ init(); } } @Override public void onRequestPermissionsResult( int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { if(requestCode==100){ if (grantResults[0]==PackageManager.PERMISSION_GRANTED){ init(); } else{ Toast.makeText(this, "No permission", Toast.LENGTH_LONG).show(); } } else { super.onRequestPermissionsResult(requestCode, permissions, grantResults); } } private void init(){ txt=(TextView)findViewById(R.id.txt); String token="請到官網取得token"; Mapbox.getInstance(this, token); map=new MahalMap(this, handler, R.drawable.locationmark); container=(RelativeLayout)findViewById(R.id.container); container.addView(map); } Handler handler=new Handler(){ @Override public void handleMessage(Message msg){ if (msg.what==1){ Bundle bundle=msg.getData(); double lat=bundle.getDouble("LAT"); double lng=bundle.getDouble("LNG"); txt.setText(String.format("%.5f:%.5f",lat,lng)); } } }; }
底下是本人專案的畫面