From 7f65fe4d4881d7cca2e81f61a67c9dd9ff75d66e Mon Sep 17 00:00:00 2001 From: Zhou Weidong <502333560@qq.com> Date: Mon, 2 Nov 2026 14:44:28 +0800 Subject: [PATCH] =?UTF-8?q?feat(media):=20=E5=9B=BE=E7=89=87=E5=8D=A0?= =?UTF-8?q?=E4=BD=8D=E6=94=B9=E4=B8=BA=E7=BA=AF=E8=89=B2=E5=9D=97=E5=B9=B6?= =?UTF-8?q?=E5=8E=BB=E6=8E=89=E6=89=AB=E5=85=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../widgets/image_placeholder_shimmer.dart | 86 ------------- lib/core/widgets/remote_image.dart | 54 ++------- .../widgets/feed_banner_carousel.dart | 2 +- .../discover/widgets/search_result_card.dart | 9 +- .../widgets/drama_detail_header.dart | 1 - .../filter/widgets/filter_result_card.dart | 1 - lib/pages/following/following_grid_card.dart | 1 - .../following/following_history_card.dart | 1 - .../widgets/continue_watching_banner.dart | 1 - lib/pages/home/widgets/home_drama_grid.dart | 5 +- lib/pages/home/widgets/home_feed_card.dart | 19 +-- .../mine/widgets/mine_continue_watching.dart | 1 - lib/pages/new_releases/new_releases_page.dart | 2 - .../player/widgets/drama_info_sheet.dart | 12 +- .../recommend/widgets/home_hero_carousel.dart | 6 +- .../widgets/home_recommend_content.dart | 2 - .../widgets/playlist_detail_page.dart | 1 - .../widgets/recommend_section_card.dart | 13 +- .../widgets/watch_record_card.dart | 8 +- test/core/image_placeholder_shimmer_test.dart | 114 ------------------ 20 files changed, 48 insertions(+), 291 deletions(-) delete mode 100644 lib/core/widgets/image_placeholder_shimmer.dart delete mode 100644 test/core/image_placeholder_shimmer_test.dart diff --git a/lib/core/widgets/image_placeholder_shimmer.dart b/lib/core/widgets/image_placeholder_shimmer.dart deleted file mode 100644 index 7956e0f..0000000 --- a/lib/core/widgets/image_placeholder_shimmer.dart +++ /dev/null @@ -1,86 +0,0 @@ -// Author: fruit.com.cn -// 职责:给默认图片加载占位叠加可暂停、局部重绘的轻量扫光。 -import 'package:flutter/material.dart'; - -const _sweepDuration = Duration(milliseconds: 1400); -const double _bandWidthRatio = 0.65; -const _highlight = Color(0x12FFFFFF); -const _clear = Color(0x00FFFFFF); - -/// 只用于等待真实图片的占位;减少动画或页面离屏时不运行扫光。 -class ImagePlaceholderShimmer extends StatefulWidget { - /// 在 [child] 上方绘制不影响布局与点击区域的扫光。 - const ImagePlaceholderShimmer({super.key, required this.child}); - - final Widget child; - - @override - State createState() => - _ImagePlaceholderShimmerState(); -} - -class _ImagePlaceholderShimmerState extends State - with SingleTickerProviderStateMixin { - late final AnimationController _controller = AnimationController( - vsync: this, - duration: _sweepDuration, - ); - bool _active = false; - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - _active = - TickerMode.valuesOf(context).enabled && - !MediaQuery.disableAnimationsOf(context); - if (_active) { - _controller.repeat(); - } else { - _controller.stop(); - } - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - if (!_active) return widget.child; - return RepaintBoundary( - child: ClipRect( - child: CustomPaint( - foregroundPainter: _SweepPainter(_controller), - child: widget.child, - ), - ), - ); - } -} - -class _SweepPainter extends CustomPainter { - _SweepPainter(this.progress) : super(repaint: progress); - - final Animation progress; - - @override - void paint(Canvas canvas, Size size) { - if (size.isEmpty) return; - final bandWidth = size.width * _bandWidthRatio; - final left = -bandWidth + (size.width + bandWidth) * progress.value; - final rect = Rect.fromLTWH(left, 0, bandWidth, size.height); - final paint = Paint() - ..shader = const LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [_clear, _highlight, _clear], - ).createShader(rect); - canvas.drawRect(rect, paint); - } - - @override - bool shouldRepaint(covariant _SweepPainter oldDelegate) => - oldDelegate.progress != progress; -} diff --git a/lib/core/widgets/remote_image.dart b/lib/core/widgets/remote_image.dart index 097883a..d307ec9 100644 --- a/lib/core/widgets/remote_image.dart +++ b/lib/core/widgets/remote_image.dart @@ -1,10 +1,10 @@ // Author: fruit.com.cn +// 职责:统一网络图片入口,补服务地址,并在加载中/失败时显示纯色占位块。 import 'package:flutter/material.dart'; import '../config/app_config.dart'; import '../theme/app_colors.dart'; -import 'image_placeholder_shimmer.dart'; const int _minImageDecodePixels = 96; const int _maxImageDecodePixels = 2048; @@ -32,8 +32,6 @@ class RemoteImage extends StatelessWidget { this.width, this.height, this.fit = BoxFit.cover, - this.showFallbackIcon = false, - this.fallbackIconKey, this.fallbackAspectRatio, this.fallbackColor = AppColors.raised, this.fallbackWidget, @@ -45,12 +43,10 @@ class RemoteImage extends StatelessWidget { final double? width; final double? height; final BoxFit fit; - final bool showFallbackIcon; - final Key? fallbackIconKey; final double? fallbackAspectRatio; final Color fallbackColor; - /// 自定义兜底:地址为空、加载中、加载失败时都显示它(优先于 [fallbackColor])。 + /// 自定义占位:地址为空、加载中、加载失败时都显示它(优先于 [fallbackColor])。 final Widget? fallbackWidget; final int? decodeWidth; final int? decodeHeight; @@ -58,7 +54,7 @@ class RemoteImage extends StatelessWidget { @override Widget build(BuildContext context) { final url = AppConfig.mediaUrl(path); - if (url.isEmpty) return _fallback(brand: showFallbackIcon); + if (url.isEmpty) return _fallback(); return LayoutBuilder( builder: (context, constraints) { final pixelRatio = MediaQuery.devicePixelRatioOf(context); @@ -100,51 +96,23 @@ class RemoteImage extends StatelessWidget { ?.clamp(_minImageDecodePixels, _maxImageDecodePixels) .toInt(), gaplessPlayback: true, - errorBuilder: (_, _, _) => _fallback(brand: showFallbackIcon), + errorBuilder: (_, _, _) => _fallback(), frameBuilder: (context, child, frame, wasSynchronouslyLoaded) { if (wasSynchronouslyLoaded || frame != null) return child; - return _loadingFallback(); + return _fallback(); }, ); }, ); } - Widget _loadingFallback() { + Widget _fallback() { final custom = fallbackWidget; - if (custom != null) return custom; - return ImagePlaceholderShimmer(child: _fallback(brand: showFallbackIcon)); - } - - Widget _fallback({required bool brand}) { - final custom = fallbackWidget; - if (custom != null) return custom; - final mark = brand - ? LayoutBuilder( - builder: (context, constraints) { - final width = constraints.maxWidth.isFinite - ? constraints.maxWidth - : 120.0; - return Center( - child: Image.asset( - 'assets/icon/splash_logo.png', - key: fallbackIconKey, - width: width * 0.72, - fit: BoxFit.contain, - ), - ); - }, - ) - : null; - final fallback = Container( - width: width, - height: height, - color: brand ? AppColors.mediaBlack : fallbackColor, - alignment: Alignment.center, - child: mark, - ); + final content = custom != null + ? SizedBox(width: width, height: height, child: custom) + : Container(width: width, height: height, color: fallbackColor); final ratio = fallbackAspectRatio; - if (ratio == null) return fallback; - return AspectRatio(aspectRatio: ratio, child: fallback); + if (ratio == null) return content; + return AspectRatio(aspectRatio: ratio, child: content); } } diff --git a/lib/pages/discover/widgets/feed_banner_carousel.dart b/lib/pages/discover/widgets/feed_banner_carousel.dart index 839071c..a5ae199 100644 --- a/lib/pages/discover/widgets/feed_banner_carousel.dart +++ b/lib/pages/discover/widgets/feed_banner_carousel.dart @@ -176,7 +176,7 @@ class _FeedBannerCarouselState extends State { child: Stack( fit: StackFit.expand, children: [ - RemoteImage(banner.imageUrl, showFallbackIcon: true), + RemoteImage(banner.imageUrl), DecoratedBox( decoration: BoxDecoration( gradient: LinearGradient( diff --git a/lib/pages/discover/widgets/search_result_card.dart b/lib/pages/discover/widgets/search_result_card.dart index bf6d0e1..634f2b4 100644 --- a/lib/pages/discover/widgets/search_result_card.dart +++ b/lib/pages/discover/widgets/search_result_card.dart @@ -37,7 +37,6 @@ class SearchResultCard extends StatelessWidget { RemoteImage( drama.posterUrl, fit: BoxFit.cover, - showFallbackIcon: true, fallbackColor: AppColors.contentSurface, ), const Positioned( @@ -60,7 +59,9 @@ class SearchResultCard extends StatelessWidget { ), const SizedBox(height: AppDimens.searchResultTitleGap), Text( - drama.localizedTitle(Localizations.localeOf(context).languageCode), + drama.localizedTitle( + Localizations.localeOf(context).languageCode, + ), key: Key('search-result-title-${drama.id}'), maxLines: 2, overflow: TextOverflow.ellipsis, @@ -93,7 +94,9 @@ class SearchResultCard extends StatelessWidget { } List _tags(BuildContext context) { - final localized = drama.localizedTypeLabels(Localizations.localeOf(context).languageCode); + final localized = drama.localizedTypeLabels( + Localizations.localeOf(context).languageCode, + ); if (localized.isNotEmpty) return localized.take(3).toList(growable: false); final seen = {}; return [drama.genre, drama.category] diff --git a/lib/pages/drama_detail/widgets/drama_detail_header.dart b/lib/pages/drama_detail/widgets/drama_detail_header.dart index 9b08c09..7c2dc34 100644 --- a/lib/pages/drama_detail/widgets/drama_detail_header.dart +++ b/lib/pages/drama_detail/widgets/drama_detail_header.dart @@ -126,7 +126,6 @@ class DramaDetailHeader extends StatelessWidget { child: RemoteImage( drama.posterUrl, fit: BoxFit.cover, - showFallbackIcon: true, fallbackAspectRatio: 3 / 4, ), ), diff --git a/lib/pages/filter/widgets/filter_result_card.dart b/lib/pages/filter/widgets/filter_result_card.dart index 0cdc2df..6d3cba0 100644 --- a/lib/pages/filter/widgets/filter_result_card.dart +++ b/lib/pages/filter/widgets/filter_result_card.dart @@ -53,7 +53,6 @@ class FilterResultCard extends StatelessWidget { RemoteImage( drama.posterUrl, fit: BoxFit.cover, - showFallbackIcon: true, fallbackAspectRatio: 3 / 4, ), if (drama.isHot) diff --git a/lib/pages/following/following_grid_card.dart b/lib/pages/following/following_grid_card.dart index ac360d8..639a794 100644 --- a/lib/pages/following/following_grid_card.dart +++ b/lib/pages/following/following_grid_card.dart @@ -64,7 +64,6 @@ class FollowingGridCard extends StatelessWidget { child: RemoteImage( posterUrl, fit: BoxFit.cover, - showFallbackIcon: true, fallbackColor: AppColors.contentSurface, ), ), diff --git a/lib/pages/following/following_history_card.dart b/lib/pages/following/following_history_card.dart index d47bbdf..43ca618 100644 --- a/lib/pages/following/following_history_card.dart +++ b/lib/pages/following/following_history_card.dart @@ -75,7 +75,6 @@ class FollowingHistoryCard extends StatelessWidget { child: RemoteImage( posterUrl, fit: BoxFit.cover, - showFallbackIcon: true, fallbackColor: AppColors.contentSurface, ), ), diff --git a/lib/pages/following/widgets/continue_watching_banner.dart b/lib/pages/following/widgets/continue_watching_banner.dart index 2097ad4..568d7a6 100644 --- a/lib/pages/following/widgets/continue_watching_banner.dart +++ b/lib/pages/following/widgets/continue_watching_banner.dart @@ -40,7 +40,6 @@ class ContinueWatchingBanner extends StatelessWidget { RemoteImage( record.displayLandscapeCover, fit: BoxFit.cover, - showFallbackIcon: true, fallbackColor: AppColors.contentSurface, ), const DecoratedBox( diff --git a/lib/pages/home/widgets/home_drama_grid.dart b/lib/pages/home/widgets/home_drama_grid.dart index b1f0980..33b688e 100644 --- a/lib/pages/home/widgets/home_drama_grid.dart +++ b/lib/pages/home/widgets/home_drama_grid.dart @@ -122,7 +122,6 @@ class HomeDramaCard extends StatelessWidget { drama.posterUrl, width: double.infinity, fit: BoxFit.fitWidth, - showFallbackIcon: true, fallbackAspectRatio: _coverAspectRatio, ), Positioned(top: 8, left: 8, child: _tagBadge()), @@ -226,7 +225,9 @@ class HomeDramaCard extends StatelessWidget { } Widget _genre(BuildContext context) { - final labels = drama.localizedTypeLabels(Localizations.localeOf(context).languageCode); + final labels = drama.localizedTypeLabels( + Localizations.localeOf(context).languageCode, + ); final genre = labels.isEmpty ? (drama.genre.isEmpty ? drama.category : drama.genre) : labels.join(' / '); diff --git a/lib/pages/home/widgets/home_feed_card.dart b/lib/pages/home/widgets/home_feed_card.dart index e8aeaa5..f957715 100644 --- a/lib/pages/home/widgets/home_feed_card.dart +++ b/lib/pages/home/widgets/home_feed_card.dart @@ -12,11 +12,7 @@ import '../../../core/widgets/remote_image.dart'; final RegExp _tagSplitter = RegExp(r'[,/、·]'); class HomeFeedCard extends StatelessWidget { - const HomeFeedCard({ - super.key, - required this.drama, - required this.onTap, - }); + const HomeFeedCard({super.key, required this.drama, required this.onTap}); final Drama drama; final VoidCallback onTap; @@ -44,7 +40,9 @@ class HomeFeedCard extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.start, children: [ Text( - drama.localizedTitle(Localizations.localeOf(context).languageCode), + drama.localizedTitle( + Localizations.localeOf(context).languageCode, + ), key: Key('home-feed-title-${drama.id}'), maxLines: 2, overflow: TextOverflow.ellipsis, @@ -69,7 +67,9 @@ class HomeFeedCard extends StatelessWidget { } Widget _cover(BuildContext context) { - final label = drama.localizedTag(Localizations.localeOf(context).languageCode); + final label = drama.localizedTag( + Localizations.localeOf(context).languageCode, + ); return ClipRRect( key: Key('home-feed-cover-clip-${drama.id}'), borderRadius: BorderRadius.circular(AppDimens.cardCoverRadius), @@ -82,7 +82,6 @@ class HomeFeedCard extends StatelessWidget { drama.posterUrl, key: Key('home-feed-cover-${drama.id}'), fit: BoxFit.cover, - showFallbackIcon: true, fallbackColor: AppColors.contentSurface, ), if (label.isNotEmpty) @@ -133,7 +132,9 @@ class HomeFeedCard extends StatelessWidget { } List _cardTags(BuildContext context) { - final localized = drama.localizedTypeLabels(Localizations.localeOf(context).languageCode); + final localized = drama.localizedTypeLabels( + Localizations.localeOf(context).languageCode, + ); if (localized.isNotEmpty) return localized.take(3).toList(growable: false); final seen = {}; return [drama.genre, drama.category] diff --git a/lib/pages/mine/widgets/mine_continue_watching.dart b/lib/pages/mine/widgets/mine_continue_watching.dart index b6c2c06..33a7f67 100644 --- a/lib/pages/mine/widgets/mine_continue_watching.dart +++ b/lib/pages/mine/widgets/mine_continue_watching.dart @@ -128,7 +128,6 @@ class MineContinueWatching extends StatelessWidget { child: RemoteImage( record.dramaCover ?? '', fit: BoxFit.cover, - showFallbackIcon: true, fallbackColor: AppColors.contentSurface, ), ), diff --git a/lib/pages/new_releases/new_releases_page.dart b/lib/pages/new_releases/new_releases_page.dart index 85c066e..fb0a648 100644 --- a/lib/pages/new_releases/new_releases_page.dart +++ b/lib/pages/new_releases/new_releases_page.dart @@ -276,7 +276,6 @@ class _NewReleasesPageState extends State { RemoteImage( drama.posterUrl, fit: BoxFit.cover, - showFallbackIcon: true, fallbackAspectRatio: 16 / 9, ), const DecoratedBox( @@ -448,7 +447,6 @@ class _NewReleasesPageState extends State { RemoteImage( drama.posterUrl, fit: BoxFit.cover, - showFallbackIcon: true, fallbackAspectRatio: 3 / 4, ), Positioned( diff --git a/lib/pages/player/widgets/drama_info_sheet.dart b/lib/pages/player/widgets/drama_info_sheet.dart index 18a9b03..03caee8 100644 --- a/lib/pages/player/widgets/drama_info_sheet.dart +++ b/lib/pages/player/widgets/drama_info_sheet.dart @@ -255,11 +255,7 @@ class _DramaInfoSheetState extends State { width: _posterWidth, child: AspectRatio( aspectRatio: 3 / 4, - child: RemoteImage( - drama.posterUrl, - fit: BoxFit.cover, - showFallbackIcon: true, - ), + child: RemoteImage(drama.posterUrl, fit: BoxFit.cover), ), ), ), @@ -466,11 +462,7 @@ class _DramaInfoSheetState extends State { width: _relatedWidth, child: AspectRatio( aspectRatio: 3 / 4, - child: RemoteImage( - drama.posterUrl, - fit: BoxFit.cover, - showFallbackIcon: true, - ), + child: RemoteImage(drama.posterUrl, fit: BoxFit.cover), ), ), ), diff --git a/lib/pages/recommend/widgets/home_hero_carousel.dart b/lib/pages/recommend/widgets/home_hero_carousel.dart index 37e5d87..77b9d0f 100644 --- a/lib/pages/recommend/widgets/home_hero_carousel.dart +++ b/lib/pages/recommend/widgets/home_hero_carousel.dart @@ -187,7 +187,6 @@ class _HomeHeroCarouselState extends State { RemoteImage( banner.imageUrl, fit: BoxFit.cover, - showFallbackIcon: true, fallbackColor: AppColors.contentSurface, ), const DecoratedBox( @@ -210,10 +209,7 @@ class _HomeHeroCarouselState extends State { left: 28, right: 28, bottom: _copyBottomInset, - child: _HeroCopy( - key: Key('home-hero-copy-$index'), - drama: drama, - ), + child: _HeroCopy(key: Key('home-hero-copy-$index'), drama: drama), ), if (drama != null) Positioned.fill( diff --git a/lib/pages/recommend/widgets/home_recommend_content.dart b/lib/pages/recommend/widgets/home_recommend_content.dart index 181c416..8e4cd88 100644 --- a/lib/pages/recommend/widgets/home_recommend_content.dart +++ b/lib/pages/recommend/widgets/home_recommend_content.dart @@ -97,7 +97,6 @@ class HomePosterCard extends StatelessWidget { RemoteImage( drama.posterUrl, fit: BoxFit.cover, - showFallbackIcon: true, fallbackColor: AppColors.contentSurface, ), if (tagLabel.isNotEmpty) @@ -189,7 +188,6 @@ class HomeEditorPickBanner extends StatelessWidget { RemoteImage( drama.posterUrl, fit: BoxFit.cover, - showFallbackIcon: true, fallbackColor: AppColors.contentSurface, ), const DecoratedBox( diff --git a/lib/pages/recommend/widgets/playlist_detail_page.dart b/lib/pages/recommend/widgets/playlist_detail_page.dart index d847754..17c62e0 100644 --- a/lib/pages/recommend/widgets/playlist_detail_page.dart +++ b/lib/pages/recommend/widgets/playlist_detail_page.dart @@ -310,7 +310,6 @@ class _PlaylistCollage extends StatelessWidget { child: RemoteImage( drama.posterUrl, fit: BoxFit.cover, - showFallbackIcon: true, fallbackColor: _filmBody, ), ); diff --git a/lib/pages/recommend/widgets/recommend_section_card.dart b/lib/pages/recommend/widgets/recommend_section_card.dart index eaf53c1..9adb90a 100644 --- a/lib/pages/recommend/widgets/recommend_section_card.dart +++ b/lib/pages/recommend/widgets/recommend_section_card.dart @@ -25,7 +25,9 @@ class RecommendSectionCard extends StatelessWidget { @override Widget build(BuildContext context) { - final tagLabel = drama.localizedTag(Localizations.localeOf(context).languageCode); + final tagLabel = drama.localizedTag( + Localizations.localeOf(context).languageCode, + ); return SizedBox( width: AppDimens.recommendCardWidth, child: InkWell( @@ -47,7 +49,6 @@ class RecommendSectionCard extends StatelessWidget { RemoteImage( drama.posterUrl, fit: BoxFit.cover, - showFallbackIcon: true, fallbackColor: AppColors.contentSurface, ), if (tagLabel.isNotEmpty) @@ -80,7 +81,9 @@ class RecommendSectionCard extends StatelessWidget { ), const SizedBox(height: AppDimens.cardCoverTitleGap), Text( - drama.localizedTitle(Localizations.localeOf(context).languageCode), + drama.localizedTitle( + Localizations.localeOf(context).languageCode, + ), key: Key('recommend-title-$sectionKey-${drama.id}'), maxLines: 2, overflow: TextOverflow.ellipsis, @@ -116,7 +119,9 @@ class RecommendSectionCard extends StatelessWidget { } List _cardTags(BuildContext context) { - final localized = drama.localizedTypeLabels(Localizations.localeOf(context).languageCode); + final localized = drama.localizedTypeLabels( + Localizations.localeOf(context).languageCode, + ); if (localized.isNotEmpty) return localized.take(3).toList(growable: false); final seen = {}; return [drama.genre, drama.category] diff --git a/lib/pages/watch_records/widgets/watch_record_card.dart b/lib/pages/watch_records/widgets/watch_record_card.dart index 7300b39..a784374 100644 --- a/lib/pages/watch_records/widgets/watch_record_card.dart +++ b/lib/pages/watch_records/widgets/watch_record_card.dart @@ -70,9 +70,11 @@ class WatchRecordCard extends StatelessWidget { key: ValueKey('watch-record-cover-${record.dramaId}'), width: _coverWidth, height: _coverHeight, - showFallbackIcon: true, - fallbackIconKey: ValueKey( - 'watch-record-cover-placeholder-${record.dramaId}', + fallbackWidget: ColoredBox( + key: ValueKey( + 'watch-record-cover-placeholder-${record.dramaId}', + ), + color: AppColors.raised, ), ), ), diff --git a/test/core/image_placeholder_shimmer_test.dart b/test/core/image_placeholder_shimmer_test.dart deleted file mode 100644 index fac596d..0000000 --- a/test/core/image_placeholder_shimmer_test.dart +++ /dev/null @@ -1,114 +0,0 @@ -// Author: fruit.com.cn -// 职责:验证图片占位扫光生命周期及默认兜底接入。 -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:fruitshorts/core/widgets/image_placeholder_shimmer.dart'; -import 'package:fruitshorts/core/widgets/remote_image.dart'; - -const _frame = Duration(milliseconds: 350); -const double _width = 120; -const double _height = 160; -const _size = Size(_width, _height); - -Widget _placeholder({bool reduceMotion = false, bool active = true}) { - return MaterialApp( - home: MediaQuery( - data: MediaQueryData(disableAnimations: reduceMotion), - child: TickerMode( - enabled: active, - child: const Center( - child: SizedBox( - width: _width, - height: _height, - child: ImagePlaceholderShimmer( - child: ColoredBox(color: Colors.black), - ), - ), - ), - ), - ), - ); -} - -void main() { - testWidgets('should_paint_clipped_sweep_when_placeholder_is_loading', ( - tester, - ) async { - await tester.pumpWidget(_placeholder()); - final shimmer = find.byType(ImagePlaceholderShimmer); - final paintFinder = find.descendant( - of: shimmer, - matching: find.byType(CustomPaint), - ); - final paint = tester.widget(paintFinder); - var repaints = 0; - paint.foregroundPainter!.addListener(() => repaints++); - await tester.pump(_frame); - expect(repaints, greaterThan(0)); - expect(tester.getSize(shimmer), _size); - expect( - find.descendant(of: shimmer, matching: find.byType(ClipRect)), - findsOneWidget, - ); - await tester.pumpWidget(const SizedBox.shrink()); - await tester.pump(_frame); - expect(tester.binding.hasScheduledFrame, isFalse); - }); - - testWidgets('should_pause_sweep_when_motion_is_disabled_or_page_is_hidden', ( - tester, - ) async { - await tester.pumpWidget(_placeholder()); - await tester.pump(_frame); - await tester.pumpWidget(_placeholder(reduceMotion: true)); - await tester.pump(_frame); - expect(tester.binding.hasScheduledFrame, isFalse); - await tester.pumpWidget(_placeholder(active: false)); - await tester.pump(_frame); - expect(tester.binding.hasScheduledFrame, isFalse); - await tester.pumpWidget(_placeholder()); - await tester.pump(_frame); - expect(tester.binding.hasScheduledFrame, isTrue); - await tester.pumpWidget(const SizedBox.shrink()); - }); - - testWidgets( - 'should_sweep_only_default_loading_fallback_when_image_is_pending', - (tester) async { - await tester.pumpWidget( - const MaterialApp( - home: SizedBox( - width: _width, - height: _height, - child: RemoteImage('/files/pending-cover.jpg'), - ), - ), - ); - final image = tester.widget(find.byType(Image)); - final context = tester.element(find.byType(RemoteImage)); - const loaded = SizedBox(key: Key('loaded')); - expect( - image.frameBuilder!(context, loaded, null, false), - isA(), - ); - expect(image.frameBuilder!(context, loaded, 0, false), same(loaded)); - expect( - image.errorBuilder!(context, Exception('failed'), null), - isNot(isA()), - ); - await tester.pumpWidget(const MaterialApp(home: RemoteImage(''))); - expect(find.byType(ImagePlaceholderShimmer), findsNothing); - await tester.pumpWidget( - const MaterialApp( - home: RemoteImage( - '/files/custom-cover.jpg', - fallbackWidget: SizedBox(key: Key('custom')), - ), - ), - ); - expect(find.byKey(const Key('custom')), findsOneWidget); - expect(find.byType(ImagePlaceholderShimmer), findsNothing); - await tester.pumpWidget(const SizedBox.shrink()); - }, - ); -}