Improve token image display with mask and background (#3231)

* Improve token image display with mask and background

* Enhance token image bg and remove wierd halo effect that was showing, also add token bg to disclaimer and picker

* Add token bg to more locations and give another shot at halo

* fix: halo around token images

* add token background to non-transparent images only

* another shot at bg based on transparency

* refactor: enhance token image widget shape handling and add filter quality option to cake image widget

* fix: another shot
This commit is contained in:
David Adegoke
2026-06-03 14:10:49 +02:00
committed by GitHub
parent 5ab91696ff
commit 4365462279
14 changed files with 257 additions and 116 deletions
+5 -2
View File
@@ -8,6 +8,7 @@ import 'package:cake_wallet/exchange/exchange_trade_state.dart';
import 'package:cake_wallet/exchange/provider/chainflip_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/token_image_widget.dart';
import 'package:cake_wallet/new-ui/widgets/keyboard_hide_overlay.dart';
import 'package:cake_wallet/new-ui/widgets/modern_button.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
@@ -969,8 +970,10 @@ class SwapAmountBoxState extends State<SwapAmountBox> {
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CakeImageWidget(imageUrl: _selectedCurrency.iconPath ?? "",
width: 28, height: 28),
TokenImageWidget(
imageUrl: _selectedCurrency.iconPath ?? "",
size: 28,
),
SizedBox(width: 10),
Text(
currencyToShow,
@@ -6,6 +6,7 @@ import 'package:cake_wallet/new-ui/pages/bridge/bridge_amount_page.dart';
import 'package:cake_wallet/new-ui/pages/receive_page.dart';
import 'package:cake_wallet/new-ui/pages/send_page.dart';
import 'package:cake_wallet/new-ui/pages/swap_page.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/token_image_widget.dart';
import 'package:cake_wallet/new-ui/widgets/modern_button.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
@@ -77,7 +78,10 @@ class AssetDetailsModal extends StatelessWidget {
child: Stack(
children: [
if (iconPath.isNotEmpty)
CakeImageWidget(imageUrl: iconPath, width: 75, height: 75)
TokenImageWidget(
imageUrl: iconPath,
size: 75,
)
else
Container(
width: 75,
@@ -1,8 +1,8 @@
import 'dart:math' show min;
import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/asset_details_modal.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/token_image_widget.dart';
import 'package:cake_wallet/src/screens/wallet_connect/utils/string_parsing.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/wallet_base.dart';
@@ -83,47 +83,29 @@ class AssetTile extends StatelessWidget {
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 36,
height: 36,
child: Stack(
children: [
if((iconPath).isNotEmpty)
CakeImageWidget(imageUrl: iconPath)
else
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
borderRadius: BorderRadius.circular(99999)),
child: Center(
child: Text(
balance.asset.name.substring(0, min(2, balance.asset.name.length)),
style: TextStyle(
fontSize: 20, color: Theme.of(context).colorScheme.onPrimary),
)),
iconPath.isNotEmpty
? TokenImageWidget(
imageUrl: iconPath,
size: 36,
)
: Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
shape: BoxShape.circle,
),
child: Center(
child: Text(
balance.asset.name
.substring(0, min(2, balance.asset.name.length)),
style: TextStyle(
fontSize: 20,
color: Theme.of(context).colorScheme.onPrimary,
),
),
// if (chainIconPath.isNotEmpty)
// Align(
// alignment: Alignment.bottomRight,
// child: Container(
// decoration: ShapeDecoration(
// shape: RoundedSuperellipseBorder(
// borderRadius: BorderRadius.circular(5),side: BorderSide(color: Colors.black)),
// color: Colors.white),
// child: Padding(
// padding: const EdgeInsets.all(2.0),
// child: CakeImageWidget(
// imageUrl: chainIconPath,
// width: 12,
// height: 12,
// colorFilter:
// ColorFilter.mode(Colors.black, BlendMode.srcIn),
// ),
// )))
],
)),
),
),
SizedBox(width: 12.0),
Expanded(
child: Column(
@@ -1,4 +1,5 @@
import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/history_tile_base.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/token_image_widget.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/transaction_direction.dart';
@@ -77,22 +78,20 @@ class HistoryTile extends StatelessWidget {
child: Container(
width: 16,
height: 16,
decoration: BoxDecoration(color: Theme.of(context).colorScheme.onInverseSurface, shape: BoxShape.circle),
child: CakeImageWidget(imageUrl:
_getDirectionIconToken(),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.onInverseSurface, shape: BoxShape.circle),
child: CakeImageWidget(
imageUrl: _getDirectionIconToken(),
height: 14,
width: 14,
colorFilter: ColorFilter.mode(
direction == TransactionDirection.outgoing
? Theme
.of(context)
.colorScheme
.inverseSurface.withAlpha(175)
? Theme.of(context).colorScheme.inverseSurface.withAlpha(175)
: Colors.green,
BlendMode.srcIn),
width: 14,
height: 14,
),
),
),
)
)
],
);
}
@@ -101,8 +100,10 @@ class HistoryTile extends StatelessWidget {
return Stack(children: [
Opacity(
opacity: pending ? 0.5 : 1,
child: CakeImageWidget(imageUrl: asset?.iconPath ??"", width: 34,
height: 34,),
child: CakeImageWidget(
imageUrl: asset?.iconPath ?? "",
width: 34,
),
),
Align(
alignment: Alignment.bottomRight,
@@ -1,13 +1,13 @@
import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item.dart';
import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/token_image_widget.dart';
import 'package:cake_wallet/new-ui/widgets/copy_wrapper.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/transaction_details/confirmations_list_item.dart';
import 'package:cake_wallet/src/screens/transaction_details/transaction_details_list_item.dart';
import 'package:cake_wallet/src/screens/transaction_details/address_list_item.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cake_wallet/src/widgets/new_list_row/new_list_section.dart';
import 'package:cake_wallet/utils/address_formatter.dart';
import 'package:cake_wallet/view_model/transaction_details_view_model.dart';
@@ -71,11 +71,12 @@ class _TransactionDetailsModalState extends State<TransactionDetailsModal> {
controller: controller,
child: Column(
children: [
CakeImageWidget(
imageUrl: widget.transactionDetailsViewModel.transactionAsset.iconPath ??
"",
width: 64,
height: 64),
TokenImageWidget(
imageUrl: widget
.transactionDetailsViewModel.transactionAsset.iconPath ??
"",
size: 64,
),
SizedBox(height: 10),
Text(
widget.transactionDetailsViewModel.formattedTitle +
@@ -0,0 +1,156 @@
import 'dart:async';
import 'dart:ui' as ui;
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:flutter/material.dart';
enum _IconShape {
alreadyCircular,
clipOnly,
hollowNeedsBackdrop,
}
class TokenImageWidget extends StatefulWidget {
const TokenImageWidget({
super.key,
required this.imageUrl,
required this.size,
this.errorWidget,
});
final String imageUrl;
final double size;
final Widget? errorWidget;
@override
State<TokenImageWidget> createState() => _TokenImageWidgetState();
}
class _TokenImageWidgetState extends State<TokenImageWidget> {
static final Map<String, _IconShape> _shapeCache = {};
_IconShape _shape = _IconShape.clipOnly;
@override
void initState() {
super.initState();
_resolveShape();
}
@override
void didUpdateWidget(TokenImageWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.imageUrl != widget.imageUrl) {
_resolveShape();
}
}
Future<void> _resolveShape() async {
final url = widget.imageUrl;
final cached = _shapeCache[url];
if (cached != null) {
if (mounted && _shape != cached) {
setState(() => _shape = cached);
}
return;
}
final shape = await _analyzeShape(url);
_shapeCache[url] = shape;
if (mounted) {
setState(() => _shape = shape);
}
}
Future<_IconShape> _analyzeShape(String url) async {
if (url.toLowerCase().endsWith('.svg')) return _IconShape.clipOnly;
final ImageProvider provider;
if (url.startsWith('assets/')) {
provider = AssetImage(url);
} else if (url.startsWith('http')) {
provider = NetworkImage(url);
} else {
return _IconShape.clipOnly;
}
try {
final completer = Completer<ui.Image>();
final stream = provider.resolve(ImageConfiguration.empty);
late final ImageStreamListener listener;
listener = ImageStreamListener(
(info, _) {
if (!completer.isCompleted) completer.complete(info.image);
},
onError: (error, _) {
if (!completer.isCompleted) completer.completeError(error);
},
);
stream.addListener(listener);
final image = await completer.future;
stream.removeListener(listener);
final byteData = await image.toByteData(format: ui.ImageByteFormat.rawRgba);
if (byteData == null) return _IconShape.clipOnly;
final totalPixels = image.width * image.height;
var transparentPixels = 0;
for (var i = 0; i < totalPixels; i++) {
final alpha = byteData.getUint8(i * 4 + 3);
if (alpha < 128) transparentPixels++;
}
final ratio = transparentPixels / totalPixels;
if (ratio > 0.15 && ratio < 0.30) {
return _IconShape.alreadyCircular;
}
if (ratio >= 0.30) {
return _IconShape.hollowNeedsBackdrop;
}
return _IconShape.clipOnly;
} catch (e) {
printV('TokenImageWidget: failed to analyze $url: $e');
return _IconShape.clipOnly;
}
}
@override
Widget build(BuildContext context) {
final image = SizedBox(
width: widget.size,
height: widget.size,
child: CakeImageWidget(
imageUrl: widget.imageUrl,
width: widget.size,
height: widget.size,
fit: BoxFit.cover,
filterQuality: FilterQuality.high,
errorWidget: widget.errorWidget,
),
);
switch (_shape) {
case _IconShape.alreadyCircular:
return image;
case _IconShape.clipOnly:
return ClipOval(child: image);
case _IconShape.hollowNeedsBackdrop:
return Container(
width: widget.size,
height: widget.size,
clipBehavior: Clip.antiAlias,
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: Colors.white,
),
child: image,
);
}
}
}
@@ -1,5 +1,6 @@
import 'package:cake_wallet/entities/fiat_currency.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/token_image_widget.dart';
import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart';
@@ -84,14 +85,14 @@ class _ReceiveAmountModalState extends State<ReceiveAmountModal> {
Row(
spacing: 8,
children: [
Image.asset(
widget.walletAddressListViewModel.tokenCurrency
?.iconPath ??
widget.walletAddressListViewModel.currencies.first
.iconPath ??
"",
width: 32,
height: 32),
TokenImageWidget(
imageUrl: widget.walletAddressListViewModel.tokenCurrency
?.iconPath ??
widget.walletAddressListViewModel.currencies.first
.iconPath ??
"",
size: 32,
),
Text((widget.walletAddressListViewModel.tokenCurrency ??
widget.walletAddressListViewModel.currencies.first
as CryptoCurrency)
@@ -2,6 +2,7 @@ import 'dart:math';
import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/token_image_widget.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/wallet_type.dart';
@@ -177,10 +178,9 @@ class InfoboxCurrencyRow extends StatelessWidget {
color: Theme.of(context).colorScheme.surfaceContainer,
width: iconBorder),
borderRadius: BorderRadius.circular(9999999)),
child: CakeImageWidget(
imageUrl: entry.value.iconPath,
width: 24,
height: 24,
child: TokenImageWidget(
imageUrl: entry.value.iconPath ?? '',
size: 24,
),
),
))
@@ -1,3 +1,4 @@
import 'package:cake_wallet/new-ui/widgets/coins_page/token_image_widget.dart';
import 'package:cake_wallet/new-ui/widgets/send_page/floating_icon_button.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:flutter/material.dart';
@@ -109,7 +110,7 @@ class _NewSendAmountInputState extends State<NewSendAmountInput> {
spacing: 8,
children: [
if (widget.hasPicker && widget.currencyIconPath.isNotEmpty)
CakeImageWidget(imageUrl: widget.currencyIconPath, width: 24, height: 24),
TokenImageWidget(imageUrl: widget.currencyIconPath, size: 24),
Text(widget.currency),
if (widget.hasPicker)
CakeImageWidget(imageUrl:
@@ -1,5 +1,6 @@
import 'package:cake_wallet/core/address_validator.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/token_image_widget.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/address_text_field.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
@@ -396,10 +397,9 @@ class _EditTokenPageBodyState extends State<EditTokenPageBody> {
alignment: Alignment.center,
children: [
ClipOval(
child: CakeImageWidget(
child: TokenImageWidget(
imageUrl: _tokenIconPathController.text,
width: 75,
height: 75,
size: 75,
),
),
if (_isTokenVerified)
@@ -5,14 +5,11 @@ import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_regular
import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_toggle.dart';
import 'package:cake_wallet/entities/sort_balance_types.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/pages/settings_page.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/token_image_widget.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/dashboard/favorite_token_modal.dart';
import 'package:cake_wallet/src/screens/settings/widgets/settings_cell_with_arrow.dart';
import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cake_wallet/src/screens/settings/widgets/settings_picker_cell.dart';
import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart';
import 'package:cake_wallet/src/widgets/new_list_row/new_list_section.dart';
import 'package:cake_wallet/src/widgets/picker.dart';
@@ -186,30 +183,25 @@ class HomeSettingsPage extends BasePage {
'token': token,
});
},
leading: Container(
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(shape: BoxShape.circle),
child: CakeImageWidget(
imageUrl: token.iconPath,
height: 40,
width: 40,
errorWidget: Container(
height: 30.0,
width: 30.0,
child: Center(
child: Text(
token.title.substring(0, min(token.title.length, 2)),
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontSize: 11,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context).colorScheme.surfaceContainerHighest,
leading: TokenImageWidget(
imageUrl: token.iconPath ?? '',
size: 40,
errorWidget: Container(
height: 30.0,
width: 30.0,
child: Center(
child: Text(
token.title.substring(0, min(token.title.length, 2)),
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontSize: 11,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context).colorScheme.surfaceContainerHighest,
),
),
),
decoration: BoxDecoration(
@@ -1,7 +1,5 @@
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/token_image_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:qr_flutter/qr_flutter.dart' as qr;
class QrImage extends StatelessWidget {
@@ -44,10 +42,9 @@ class QrImage extends StatelessWidget {
backgroundColor: backgroundColor,
padding: const EdgeInsets.all(12.0),
),
CakeImageWidget(imageUrl:
imagePath,
width: logoSize * 0.8,
height: logoSize * 0.8,
TokenImageWidget(
imageUrl: imagePath,
size: logoSize * 0.8,
),
],
);
+4
View File
@@ -16,6 +16,7 @@ class CakeImageWidget extends StatelessWidget {
this.borderRadius = 24.0,
this.alignment,
this.allowDrawingOutsideViewBox,
this.filterQuality,
});
final String? imageUrl;
@@ -29,6 +30,7 @@ class CakeImageWidget extends StatelessWidget {
final AlignmentGeometry? alignment;
final bool? allowDrawingOutsideViewBox;
final double borderRadius;
final FilterQuality? filterQuality;
@override
Widget build(BuildContext context) {
@@ -69,6 +71,7 @@ class CakeImageWidget extends StatelessWidget {
width: width,
fit: fit,
color: color,
filterQuality: filterQuality ?? FilterQuality.medium,
errorBuilder: (_, __, ___) => _buildErrorWidget(context),
);
}
@@ -93,6 +96,7 @@ class CakeImageWidget extends StatelessWidget {
width: width,
fit: fit ?? BoxFit.cover,
color: color,
filterQuality: filterQuality ?? FilterQuality.medium,
loadingBuilder: (_, Widget child, ImageChunkEvent? progress) {
if (progress == null) return child;
return loadingWidget ?? const Center(child: CircularProgressIndicator());
+3 -4
View File
@@ -3,7 +3,7 @@
import 'dart:math';
import 'package:cake_wallet/entities/seed_type.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/token_image_widget.dart';
import 'package:cake_wallet/src/widgets/search_bar_widget.dart';
import 'package:cake_wallet/themes/core/theme_extension.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart';
@@ -498,10 +498,9 @@ class _PickerState<Item> extends State<Picker<Item>> {
Widget? _getItemIcon(Item item) {
if (item is Currency) {
if (item.iconPath != null) {
return CakeImageWidget(
return TokenImageWidget(
imageUrl: item.iconPath!,
height: 20.0,
width: 20.0,
size: 20.0,
);
} else {
return Container(