105 lines
3.8 KiB
Dart
105 lines
3.8 KiB
Dart
import 'dart:async';
|
||
import 'dart:ui' as ui;
|
||
|
||
import 'package:flutter/foundation.dart';
|
||
import 'package:flutter/painting.dart';
|
||
import 'package:flutter_map/flutter_map.dart';
|
||
import 'package:http/http.dart' as http;
|
||
|
||
/// ESRI World Imagery 底图地址(国内网络可能不可达)
|
||
const String kEsriWorldImageryUrl =
|
||
'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}';
|
||
|
||
/// 网络超时时长(瓦片加载)
|
||
const Duration _kTileTimeout = Duration(seconds: 8);
|
||
|
||
/// ─────────────────────────────────────────────────────────
|
||
/// 容错网络瓦片图
|
||
/// ─────────────────────────────────────────────────────────
|
||
///
|
||
/// 加载失败(超时 / 断网 / 404 等)时返回 1×1 透明 PNG,
|
||
/// 避免 SocketException 刷屏并保证地图其余部分正常渲染。
|
||
class ResilientNetworkImage extends ImageProvider<ResilientNetworkImage> {
|
||
const ResilientNetworkImage(this.url);
|
||
|
||
final String url;
|
||
|
||
@override
|
||
Future<ResilientNetworkImage> obtainKey(ImageConfiguration configuration) =>
|
||
SynchronousFuture<ResilientNetworkImage>(this);
|
||
|
||
@override
|
||
ImageStreamCompleter loadImage(
|
||
ResilientNetworkImage key,
|
||
ImageDecoderCallback decode,
|
||
) {
|
||
return MultiFrameImageStreamCompleter(
|
||
codec: _loadAsync(decode),
|
||
scale: 1.0,
|
||
debugLabel: 'ResilientNetworkImage(${url.split('/').last})',
|
||
);
|
||
}
|
||
|
||
Future<ui.Codec> _loadAsync(ImageDecoderCallback decode) async {
|
||
try {
|
||
final client = http.Client();
|
||
final response = await client
|
||
.get(Uri.parse(url))
|
||
.timeout(_kTileTimeout);
|
||
|
||
if (response.statusCode == 200 && response.bodyBytes.isNotEmpty) {
|
||
final buffer = await ui.ImmutableBuffer.fromUint8List(
|
||
response.bodyBytes,
|
||
);
|
||
return decode(buffer);
|
||
}
|
||
} catch (_) {
|
||
// 超时 / 网络异常 / 解码失败 → 走下面的透明兜底
|
||
}
|
||
// 1×1 透明 PNG,避免 Flutter 框架抛出 MissingImageException
|
||
return decode(
|
||
await ui.ImmutableBuffer.fromUint8List(_transparentPng),
|
||
);
|
||
}
|
||
|
||
@override
|
||
bool operator ==(Object other) =>
|
||
other is ResilientNetworkImage && other.url == url;
|
||
|
||
@override
|
||
int get hashCode => url.hashCode;
|
||
}
|
||
|
||
/// ─────────────────────────────────────────────────────────
|
||
/// 容错 TileProvider —— 直接在 TileLayer.tileProvider 中使用
|
||
/// ─────────────────────────────────────────────────────────
|
||
///
|
||
/// ```dart
|
||
/// TileLayer(
|
||
/// urlTemplate: kEsriWorldImageryUrl,
|
||
/// tileProvider: ResilientTileProvider(),
|
||
/// minZoom: 0,
|
||
/// maxZoom: 19,
|
||
/// )
|
||
/// ```
|
||
class ResilientTileProvider extends TileProvider {
|
||
@override
|
||
ImageProvider<Object> getImage(TileCoordinates coordinates, TileLayer options) {
|
||
final url = getTileUrl(coordinates, options);
|
||
return ResilientNetworkImage(url);
|
||
}
|
||
}
|
||
|
||
/// 1×1 透明 PNG 字节(兜底用)
|
||
final Uint8List _transparentPng = Uint8List.fromList(<int>[
|
||
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG signature
|
||
0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, // IHDR chunk
|
||
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, // 1×1
|
||
0x08, 0x06, 0x00, 0x00, 0x00, 0x1F, 0x15, 0xC4, // RGBA 8bit
|
||
0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, 0x41, // IDAT chunk
|
||
0x54, 0x78, 0x9C, 0x62, 0x00, 0x00, 0x00, 0x02,
|
||
0x00, 0x01, 0xE5, 0x27, 0xDE, 0xFC, 0x00, 0x00, // compressed data
|
||
0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, // IEND chunk
|
||
0x60, 0x82,
|
||
]);
|