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

105 lines
3.8 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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