Files
feature-next-arch/lib/features/home/presentation/widgets/map/resilient_tile_provider.dart

105 lines
3.8 KiB
Dart
Raw Normal View History

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,
]);