MapBox for Android

      在〈MapBox for Android〉中尚無留言

Mapbox樣貌

Mapbox地圖相當漂亮, 跟Google不相上下, 見下圖

mapbox

不再使用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));
            }
        }
    };
}

底下是本人專案的畫面

mapbox2

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *