dev
dart 168 lines 5.58 KB
Raw
1 import 'package:flutter/material.dart';
2 import 'package:flutter_svg/flutter_svg.dart';
3 import 'package:vector_graphics/vector_graphics.dart';
4
5 class CakeImageWidget extends StatelessWidget {
6 const CakeImageWidget({
7 super.key,
8 this.imageUrl,
9 this.height,
10 this.width,
11 this.fit,
12 this.loadingWidget,
13 this.errorWidget,
14 this.color,
15 this.colorFilter,
16 this.borderRadius = 24.0,
17 this.alignment,
18 this.allowDrawingOutsideViewBox,
19 this.filterQuality,
20 this.semanticsLabel,
21 });
22
23 final String? imageUrl;
24 final double? height;
25 final double? width;
26 final BoxFit? fit;
27 final Widget? loadingWidget;
28 final Widget? errorWidget;
29 final Color? color;
30 final ColorFilter? colorFilter;
31 final AlignmentGeometry? alignment;
32 final bool? allowDrawingOutsideViewBox;
33 final double borderRadius;
34 final FilterQuality? filterQuality;
35
36 /// Accessible name for this image.
37 ///
38 /// Leave `null` (the default) for decorative imagery — the image then
39 /// contributes nothing at all to the semantics tree, so screen readers do not
40 /// stop on an unnamed node. Pass a localized string only when the image is the
41 /// sole carrier of information for the user.
42 final String? semanticsLabel;
43
44 bool get _isDecorative => semanticsLabel == null;
45
46 @override
47 Widget build(BuildContext context) {
48 if (imageUrl == null || imageUrl!.isEmpty) {
49 return _buildErrorWidget(context);
50 }
51
52 final isSvg = imageUrl!.toLowerCase().endsWith('.svg');
53 final isAsset = imageUrl!.startsWith('assets/');
54 final effectiveColorFilter =
55 colorFilter ?? (color != null ? ColorFilter.mode(color!, BlendMode.srcIn) : null);
56
57 Widget imageWidget;
58 if (isAsset) {
59 if (isSvg) {
60 imageWidget = SvgPicture(AssetBytesLoader("${imageUrl}.vec"),
61 height: height,
62 width: width,
63 alignment: alignment ?? Alignment.center,
64 allowDrawingOutsideViewBox: allowDrawingOutsideViewBox ?? false,
65 colorFilter: effectiveColorFilter,
66 semanticsLabel: semanticsLabel,
67 excludeFromSemantics: _isDecorative,
68 fit: fit ?? BoxFit.contain, errorBuilder: (context, e, trace) {
69 return SvgPicture.asset(
70 imageUrl!,
71 height: height,
72 alignment: alignment ?? Alignment.center,
73 allowDrawingOutsideViewBox: allowDrawingOutsideViewBox ?? false,
74 width: width,
75 errorBuilder: (_, __, ___) => SizedBox(height: height, width: width),
76 colorFilter: effectiveColorFilter,
77 semanticsLabel: semanticsLabel,
78 excludeFromSemantics: _isDecorative,
79 fit: fit ?? BoxFit.contain,
80 );
81 });
82 } else {
83 imageWidget = Image.asset(
84 imageUrl!,
85 height: height,
86 width: width,
87 fit: fit,
88 color: color,
89 filterQuality: filterQuality ?? FilterQuality.medium,
90 semanticLabel: semanticsLabel,
91 excludeFromSemantics: _isDecorative,
92 errorBuilder: (_, __, ___) => _buildErrorWidget(context),
93 );
94 }
95 } else {
96 imageWidget = isSvg
97 ? SvgPicture.network(
98 imageUrl!,
99 height: height,
100 width: width,
101 colorFilter: effectiveColorFilter,
102 alignment: alignment ?? Alignment.center,
103 allowDrawingOutsideViewBox: allowDrawingOutsideViewBox ?? false,
104 fit: fit ?? BoxFit.contain,
105 semanticsLabel: semanticsLabel,
106 excludeFromSemantics: _isDecorative,
107 placeholderBuilder: (_) => _buildLoadingWidget(),
108 errorBuilder: (_, __, ___) => _buildErrorWidget(context),
109 )
110 : Image.network(
111 imageUrl!,
112 height: height,
113 width: width,
114 fit: fit ?? BoxFit.cover,
115 color: color,
116 filterQuality: filterQuality ?? FilterQuality.medium,
117 semanticLabel: semanticsLabel,
118 excludeFromSemantics: _isDecorative,
119 loadingBuilder: (_, Widget child, ImageChunkEvent? progress) {
120 if (progress == null) return child;
121 return _buildLoadingWidget();
122 },
123 errorBuilder: (_, __, ___) => _buildErrorWidget(context),
124 );
125 }
126
127 return imageWidget;
128 }
129
130 /// A caller-supplied [loadingWidget] owns its own semantics; the built-in
131 /// spinner is purely visual and must not become an unnamed focus stop.
132 Widget _buildLoadingWidget() =>
133 loadingWidget ?? const ExcludeSemantics(child: Center(child: CircularProgressIndicator()));
134
135 Widget _buildErrorWidget(BuildContext context) {
136 final Widget placeholder = Container(
137 height: height,
138 width: width,
139 decoration: BoxDecoration(
140 borderRadius: BorderRadius.circular(borderRadius),
141 color: Theme.of(context).colorScheme.surfaceContainerHighest,
142 ),
143 child: Center(
144 child: errorWidget ??
145 Icon(
146 Icons.error_outline,
147 color: Theme.of(context).colorScheme.error,
148 size: 24,
149 ),
150 ),
151 );
152
153 // Several call sites render meaningful content (e.g. the asset's initials) as
154 // their [errorWidget], so leave its semantics to the caller.
155 if (errorWidget != null) {
156 return placeholder;
157 }
158
159 return _isDecorative
160 ? ExcludeSemantics(child: placeholder)
161 : Semantics(
162 container: true,
163 image: true,
164 label: semanticsLabel,
165 child: ExcludeSemantics(child: placeholder),
166 );
167 }
168 }