Open9

【Flutter】地図機能関連

heyhey1028heyhey1028

APIキーの隠蔽

iOS

  • 環境変数用ファイルを作成(xcodeからファイルを追加)
Runner/Environment.swift
import Foundation

struct Env {
  static let googleMapApiKey = "<APIキー>"
}
  • AppDelegate内で環境変数用ファイルにアクセスし取得
Runner/AppDelegate.swift
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    GMSServices.provideAPIKey(Env.googleMapApiKey) // <--- 
    GeneratedPluginRegistrant.register(with: self) 
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
  • Environment.swiftをgitignore
Runner/Environment.swift

Android

  • 環境変数用ファイルを作成
android/secret.properties
googleMap.apiKey=<APIキー>
  • 環境変数を起動時にsecret.propertiesから読み込み
android/app/build.gradle
def secretProperties = new Properties()
def secretPropertiesFile = rootProject.file('secret.properties')
if (secretPropertiesFile.exists()) {
    secretPropertiesFile.withReader('UTF-8') { reader ->
        secretProperties.load(reader)
    }
}

android{
   ...
  defaultConfig{
        ...    
        manifestPlaceholders += [
            googleMapApiKey: secretProperties.getProperty('googleMap.apiKey'),
        ]
       ...
  }
}
  • AndroidManifest内で変数に格納した環境変数を取得
  • なぜかは分からないけど、applicationNameを削除する必要有り
android/app/src/main/AndroidManifest.xml
+ <meta-data android:name="com.google.android.geo.API_KEY" android:value="${googleMapApiKey}"/>
  • secret.propertiesをgitignore
secret.properties
heyhey1028heyhey1028

GoogleMap

必要な要素

  • GoogleMapController
  • Position
    • initialPosition
    • cameraPosition
  • LocationSettings
  • Marker

パラメータ

  • mapType
  • initialCameraPosition
  • myLocationEnabled
  • onMapCreated
  • onCameraMoveStarted:マップをドラッグして移動した時に呼び出される。
  • onCameraMove:onCameraMoveStarted開始後に常に地図の中心の緯度と経度を取得できる。
  • onCameraIdle:マップ移動の最後に一度呼ばれる。
  • onTap:マップをタップした場所の緯度と経度が返される。
  • circles (Set<Circle>):地図上に範囲を示すのに使用
  • polygon(Set<Polygon>):マップ上に指定した形のポリゴンを表示
  • polylines(Set<Ployline>):マップ上の区間に線を表示
  • markers(Set<Marker>):マップ上にマーカーを設置

GoogleMapController

メソッド

  • animateCamera(<CameraUpdate>)
  • moveCamera(<CameraUpdate>)

クラス

  • CameraUpdate
    • .newCameraPosition(<CameraPosition>)
    • .newLatLng(<LatLng>)
  • CameraPosition
    • target <LatLng>:緯度経度
    • bearing <double>
    • tilt <double>:傾き
    • zoom <double>:ズーム

reference

https://zenn.dev/wakanao/articles/3820bcd67e4130
https://zenn.dev/slowhand/articles/f4e4e092f9b72b
https://codelabs.developers.google.com/codelabs/google-maps-in-flutter
https://future-architect.github.io/articles/20211224a/

heyhey1028heyhey1028

Geolocator

  • checkPermission
  • requestPermission
  • getPositionStream
  • isLocationServiceEnabled
  • getCurrentPosition
  • distanceBetween
heyhey1028heyhey1028

Widgetからカスタムマーカーを作る

アプローチ

  • BitmapDescriptor.fromBytes()もしくはBitmapDescriptor.fromAssets()を使って最終的にGoogleMapに渡す
  • その為にWidgetを一度画像化し、それをUint8ListのByteデータに変換して渡すというアプローチがある

解読

  • BitmapDescripterfromAssetもしくはfromBytesから生成することができる
BitmapDescriptor markerIcon = BitmapDescriptor.fromBytes(imageData);
  • imageDataUint8Listの型
  • マーカー化するwidgetのRenderObjectが存在する必要がある為、widgetを描画しなければならない
  • 描画する選択肢としては、1) Stackで重ねる、2) Transform.translateで画面外で描画する、の2つのアプローチが存在する
  • widgetを描画することで生成されるRenderObjectを再利用することができるようにリファレンスを取得する必要がある
  • RepaintBoundarywidgetを使うとラップしたwidgetの再描画を独立させることができる > これはもう少し勉強が必要。これは描画プロセスの中のLayerツリー構造が関係する模様。再描画の範囲を独立させるって感じかな。
  • 先のRenderObjectに対するリファレンスを取得する為に、アイコンにしたいwidgetをRepaintBoundaryでラップし、それにGlobalKeyを渡すことでGlobalKeyを通してアクセス可能にする
  • GlobalKeyのcurrentContext.findRenderObject()を使って、RenderRepaindBoundaryオブジェクトを取得する >>>
RenderRepaintBoundary boundary = iconKey.currentContext.findRenderObject();
  • RenderRepaintBoundaryが持つ、toImage()メソッドを使って、renderObjectImageに変換
ui.Image image = await boundary.toImage(pixelRatio: 3.0);
  • 今度はそのImageByteDataに変換
ByteData byteData = await image.toByteData(format: ui.ImageByteFormat.png);
  • このByteDataをuint8Listに変換
var pngBytes = byteData.buffer.asUint8List();
  • BitmapDescriptor.fromBytes(imageData)を使って、BitmapDescriptorに変換する

作成ステップ

  1. widgetを定義する
  2. そのwidgetをRepaintBoundaryでラップし、GlobalKeyを渡す
  3. GlobalKeyを通して、RenderRepaintBoundaryを取得する
  4. widgetからImageに変換
  5. ImageからByteDataに変換
  6. ByteDataからuint8Listに変換
  7. uint8ListからBitmapDescriptorに変換

参考

https://blog.takumma.net/flutter-google-map-widget-marker/

https://medium.com/swlh/using-material-icons-as-custom-google-map-markers-in-flutter-3e854de22e2

https://www.youtube.com/watch?v=cVAGLDuc2xE
https://www.youtube.com/watch?v=Nuni5VQXARo

heyhey1028heyhey1028

Mapbox

2つのバージョンが存在

  1. Mapbox GL JS v1と同世代
    • Mapbox Maps SDK for Android v9
    • Mapbox Maps SDK for iOS v6
  2. Mapbox GL JS v2と同世代
    • Mapbox Maps SDK for Android v10
    • Mapbox Maps SDK for iOS v10

パブリックトークン、シークレットトークンについて

Mapboxの利用をする際にはパブリックトークンシークレットトークンの両方を使う必要がある。

Public token:Read onlyの権限を有するトークン
Secret token:操作権限や使用可能URLを設定する事ができるトークン
https://docs.mapbox.com/help/faq/what-is-the-difference-between-a-public-token-and-a-secret-token/

MapBoxのSDKのインストールにMapBoxからダウンロードする権限(DOWNLOADS:READ)を付与したシークレットトークンが必要

その後、ダウンロードしたMapBox SDKの利用にはRead only権限のパブリックトークンを使用する。

導入手順

  1. Packageのインストール
  2. パブリックトークンの設定
  3. iOS, Androidの設定
  4. MapWidgetの配置

1. Packageのインストール

1-1. シークレットトークンの発行

1-2. Android用の設定

ご自身のパソコンのHOMEディレクトリにある.gradleディレクトリにgrandle.propertiesを作成し、以下の通り、シークレットトークンを設定

~/.gradle/grandle.properties
MAPBOX_DOWNLOADS_TOKEN=[発行したシークレットトークン]

https://docs.mapbox.com/android/maps/guides/install/#configure-your-secret-token

1-3. iOS用の設定

ご自身のパソコンのHOMEディレクトリに.netrcファイルを作成し、以下の通り、読み込みコマンドを設定

~/.netrc
machine api.mapbox.com
login mapbox
password [発行したシークレットトークン]

https://docs.mapbox.com/ios/maps/guides/install/#configure-your-secret-token

1-4. Packageのインストール

flutter pub add mapbox_maps_flutter

2. パブリックトークンの設定

MapboxOptions.setAccessToken(dotenv.env['MAPBOX_ACCESS_TOKEN'] ?? '');

3. iOS, Androidの設定

現在地取得の為のパーミッション設定をAndroid, iOS両方で設定
Android
AndroidManifest.xmlにて設定

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

iOS
Runner/Info.plistにて設定

<key>NSLocationWhenInUseUsageDescription</key>
<string>[Your explanation here]</string>

reference

https://zenn.dev/mapbox_japan/articles/d9ba57ca498170
https://pub.dev/packages/mapbox_maps_flutter
https://docs.mapbox.com/

heyhey1028heyhey1028

Mapboxのピンマーカー配置

ピンマーカーはMapboxではAnnotationと呼ばれており、MapboxMapクラスから生成できるPointAnnotationManagerクラスを使って、描画を行います。

final PointAnnotationManager? annotationMngr = await mapboxMap.annotations.createPointAnnotationManager();

生成手順

マーカーはPointAnnotationManagercreate()メソッドを使ってマーカーオブジェクトであるPointAnnotationOptionsクラスを地図上に描画することが出来ます。

await annotationMngr?.create(PointAnnotationsOptions(...));

描画する画像はUint8List型に変換し、PointAnnotationOptionsに渡す為、先に変換しておく必要があります。

Uint8Listへの変換
final bytes = await rootBundle.load('assets/images/annotation_pin.png');
final list = bytes.buffer.asUint8List();
PointAnnotationOptions
PointAnnotationOptions(
          geometry: Point(
              coordinates: Position(
            0.381457,
            6.687337,
          )).toJson(),
          textField: "custom-icon",
          textOffset: [0.0, -2.0],
          textColor: Colors.red.value,
          iconSize: 1.3,
          iconOffset: [0.0, -5.0],
          symbolSortKey: 10,
          image: list, // ⬅︎ こちらに渡す
)

またcreate(<PointAnnotationOptions>)の代わりにcreateMulti(List<PointAnnotationOptions>)を使うことで複数のマーカーを一度に描画させることも可能です。

createMulti()
var options = <PointAnnotationOptions>[];
for (var i = 0; i < 5; i++) {
  options.add(PointAnnotationOptions(
      geometry: createRandomPoint().toJson(), image: list));
}
annotationMngr?.createMulti(options);

reference

https://zenn.dev/mapbox_japan/articles/8d206839a11067?redirected=1#point-annotationって?