上一篇簡單介紹完地圖套件並可以簡易使用,今天就要把行程跟地圖真正串起來,讓使用者可以直覺地看到每天的活動分布。
這次的目標是:
簡單說,就是要把「一堆行程資料」變成「活生生的地圖行程」,讓使用者一眼就能掌握每天的路線。這篇我會一路記錄我怎麼做,碰到什麼坑,怎麼解。
經緯度為 double ,在 SQLite(也是 Drift 底層使用的資料庫)裡,REAL 是一個 浮點數(floating point number)型態。
class ActivitiesTable extends Table {
// 新增經緯度欄位
RealColumn get longitude => real().nullable()();
RealColumn get latitude => real().nullable()();
}
在地圖上,我們希望同時 標出每日行程的景點,並用 Polyline 將它們串成一條完整路線。以下程式碼示範如何達成:
Scaffold(
appBar: AppBar(title: const Text("行程地圖")),
body: asyncLatLngs.when(
data: (latLngs) {
if (latLngs.isEmpty) return const Center(child: Text("解析失敗"));
// 建立 Marker 集合
final markers = latLngs.asMap().entries.map((entry) {
final idx = entry.key;
final latLng = entry.value;
return Marker(
markerId: MarkerId("marker_$idx"),
position: latLng,
infoWindow: InfoWindow(title: "景點 ${idx + 1}"),
);
}).toSet();
// 建立 Polyline 將景點連線
final polyline = Polyline(
polylineId: const PolylineId("route"),
points: latLngs,
color: Colors.blue,
width: 4,
);
return GoogleMap(
initialCameraPosition: CameraPosition(
target: latLngs.first, // 預設鏡頭定位到第一個景點
zoom: 14,
),
markers: markers, // 加入所有 Marker
polylines: {polyline}, // 加入行程路線
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error: (err, stack) => Center(child: Text("錯誤: $err")),
),
);
在 Google Maps 中,如果只依靠 CameraPosition
的 zoom
參數來設定顯示範圍,往往只能固定在某一個縮放等級。若想要自動縮放並同時涵蓋所有的標記(Markers),就可以利用 LatLngBounds
搭配 CameraUpdate.newLatLngBounds
來達成所謂的 bounds fitting。
以下範例示範如何計算所有座標點的邊界(最南、最北、最西、最東),並讓地圖鏡頭自動移動與縮放到剛好包含全部標記的位置:
GoogleMap(
onMapCreated: (controller) async {
_mapController = controller;
// 建立 LatLngBounds,計算出最南、最北、最西、最東的經緯度
if (latLngs.isNotEmpty) {
double south = latLngs.first.latitude;
double north = latLngs.first.latitude;
double west = latLngs.first.longitude;
double east = latLngs.first.longitude;
for (var latLng in latLngs) {
if (latLng.latitude > north) north = latLng.latitude;
if (latLng.latitude < south) south = latLng.latitude;
if (latLng.longitude > east) east = latLng.longitude;
if (latLng.longitude < west) west = latLng.longitude;
}
final bounds = LatLngBounds(
southwest: LatLng(south, west),
northeast: LatLng(north, east),
);
// 更新地圖鏡頭,padding 可用來保留邊界空間
_mapController.animateCamera(
CameraUpdate.newLatLngBounds(bounds, 50), // padding 50
);
}
},
markers: markers,
polylines: {polyline},
)
這樣一來,不論行程中有多少個地點,地圖都會自動縮放到最佳比例,讓使用者一眼就能看到完整路線。
要讓 App 點擊 Marker 後打開原生地圖,需要先安裝 url_launcher
套件,並在 iOS 專案的 ios/Runner/Info.plist
中加入允許的 URL Scheme:
<key>LSApplicationQueriesSchemes</key>
<array>
<string>https</string>
<string>maps</string>
<string>comgooglemaps</string>
</array>
接著在建立 Marker 時,設定 onTap
事件呼叫開啟地圖的函式:
Marker(
markerId: MarkerId("marker_$idx"),
position: latLng,
infoWindow: InfoWindow(title: "景點 ${idx + 1}"),
onTap: () {
openMapsApp(latLng);
},
);
openMapsApp
函式可以使用 url_launcher
打開 Apple Maps 或 Google Maps,例如:
Future<void> openMapsApp(LatLng destination) async {
final googleMapsScheme = Uri.parse(
'comgooglemaps://?daddr=${destination.latitude},${destination.longitude}',
);
final googleMapsWeb = Uri.parse(
'https://www.google.com/maps/dir/?api=1&destination=${destination.latitude},${destination.longitude}',
);
final appleMapsScheme = Uri.parse(
'maps://?daddr=${destination.latitude},${destination.longitude}&dirflg=d',
);
try {
// 先檢查 Google Maps app
if (await canLaunchUrl(googleMapsScheme)) {
await launchUrl(googleMapsScheme);
return;
}
// iOS 原生地圖
if (await canLaunchUrl(appleMapsScheme)) {
await launchUrl(appleMapsScheme);
return;
}
// fallback 網頁
await launchUrl(googleMapsWeb, mode: LaunchMode.externalApplication);
} catch (e) {
// ignore
}
}
這樣使用者點擊 Marker 就能直接跳到原生地圖導航。
Android | iOS |
---|---|
![]() |
![]() |