dev
dart 162 lines 4.16 KB
Raw
1 import 'dart:async';
2 import 'dart:ui' as ui;
3
4 import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
5 import 'package:cw_core/utils/print_verbose.dart';
6 import 'package:flutter/material.dart';
7
8 enum _IconShape {
9 alreadyCircular,
10 clipOnly,
11 hollowNeedsBackdrop,
12 }
13
14 class TokenImageWidget extends StatefulWidget {
15 const TokenImageWidget({
16 super.key,
17 required this.imageUrl,
18 required this.size,
19 this.errorWidget,
20 this.semanticsLabel,
21 });
22
23 final String imageUrl;
24 final double size;
25 final Widget? errorWidget;
26
27 /// Accessible name for the token artwork. Leave `null` (the default) when the
28 /// token is already named by adjacent text, so the image stays decorative.
29 final String? semanticsLabel;
30
31 @override
32 State<TokenImageWidget> createState() => _TokenImageWidgetState();
33 }
34
35 class _TokenImageWidgetState extends State<TokenImageWidget> {
36 static final Map<String, _IconShape> _shapeCache = {};
37
38 _IconShape _shape = _IconShape.clipOnly;
39
40 @override
41 void initState() {
42 super.initState();
43 _resolveShape();
44 }
45
46 @override
47 void didUpdateWidget(TokenImageWidget oldWidget) {
48 super.didUpdateWidget(oldWidget);
49 if (oldWidget.imageUrl != widget.imageUrl) {
50 _resolveShape();
51 }
52 }
53
54 Future<void> _resolveShape() async {
55 final url = widget.imageUrl;
56
57 final cached = _shapeCache[url];
58 if (cached != null) {
59 if (mounted && _shape != cached) {
60 setState(() => _shape = cached);
61 }
62 return;
63 }
64
65 final shape = await _analyzeShape(url);
66 _shapeCache[url] = shape;
67 if (mounted) {
68 setState(() => _shape = shape);
69 }
70 }
71
72 Future<_IconShape> _analyzeShape(String url) async {
73 if (url.toLowerCase().endsWith('.svg')) return _IconShape.alreadyCircular;
74
75 final ImageProvider provider;
76 if (url.startsWith('assets/')) {
77 provider = AssetImage(url);
78 } else if (url.startsWith('http')) {
79 provider = NetworkImage(url);
80 } else {
81 return _IconShape.clipOnly;
82 }
83
84 try {
85 final completer = Completer<ui.Image>();
86 final stream = provider.resolve(ImageConfiguration.empty);
87 late final ImageStreamListener listener;
88 listener = ImageStreamListener(
89 (info, _) {
90 if (!completer.isCompleted) completer.complete(info.image);
91 },
92 onError: (error, _) {
93 if (!completer.isCompleted) completer.completeError(error);
94 },
95 );
96 stream.addListener(listener);
97
98 final image = await completer.future;
99 stream.removeListener(listener);
100
101 final byteData = await image.toByteData(format: ui.ImageByteFormat.rawRgba);
102 if (byteData == null) return _IconShape.clipOnly;
103
104 final totalPixels = image.width * image.height;
105 var transparentPixels = 0;
106 for (var i = 0; i < totalPixels; i++) {
107 final alpha = byteData.getUint8(i * 4 + 3);
108 if (alpha < 128) transparentPixels++;
109 }
110
111 final ratio = transparentPixels / totalPixels;
112
113 if (ratio > 0.15 && ratio < 0.30) {
114 return _IconShape.alreadyCircular;
115 }
116
117 if (ratio >= 0.30) {
118 return _IconShape.hollowNeedsBackdrop;
119 }
120
121 return _IconShape.clipOnly;
122 } catch (e) {
123 printV('TokenImageWidget: failed to analyze $url: $e');
124 return _IconShape.clipOnly;
125 }
126 }
127
128 @override
129 Widget build(BuildContext context) {
130 final image = SizedBox(
131 width: widget.size,
132 height: widget.size,
133 child: CakeImageWidget(
134 imageUrl: widget.imageUrl,
135 width: widget.size,
136 height: widget.size,
137 fit: BoxFit.cover,
138 filterQuality: FilterQuality.high,
139 errorWidget: widget.errorWidget,
140 semanticsLabel: widget.semanticsLabel,
141 ),
142 );
143
144 switch (_shape) {
145 case _IconShape.alreadyCircular:
146 return image;
147 case _IconShape.clipOnly:
148 return ClipOval(child: image);
149 case _IconShape.hollowNeedsBackdrop:
150 return Container(
151 width: widget.size,
152 height: widget.size,
153 clipBehavior: Clip.antiAlias,
154 decoration: const BoxDecoration(
155 shape: BoxShape.circle,
156 color: Colors.white,
157 ),
158 child: image,
159 );
160 }
161 }
162 }