diff --git a/lib/core/widgets/drama_grid_skeleton.dart b/lib/core/widgets/drama_grid_skeleton.dart index 187dfe6..490bfda 100644 --- a/lib/core/widgets/drama_grid_skeleton.dart +++ b/lib/core/widgets/drama_grid_skeleton.dart @@ -11,6 +11,7 @@ const Duration _shimmerPeriod = Duration(milliseconds: 1400); const double _shimmerTravel = 2.4; const double _shimmerStart = -1.2; const double _subtitleWidthFactor = 0.62; +const double _shimmerPhaseStep = 0.13; const Color _shimmerHighlight = Color(0x12FFFFFF); /// 首页短剧网格的占位。行数至少铺满可视高度,并带一道扫光。 @@ -53,21 +54,19 @@ class _DramaGridSkeletonState extends State Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { - final grid = _grid(constraints.maxWidth, constraints.maxHeight); return AnimatedBuilder( animation: _shimmer, - builder: (context, child) => ShaderMask( - blendMode: BlendMode.srcATop, - shaderCallback: (bounds) => _shader(bounds), - child: child, + builder: (context, _) => _grid( + constraints.maxWidth, + constraints.maxHeight, + _shimmer.value, ), - child: grid, ); }, ); } - Widget _grid(double width, double height) { + Widget _grid(double width, double height, double shimmerValue) { final itemWidth = _itemWidth(width); final itemExtent = _itemExtent(itemWidth); return GridView.builder( @@ -85,7 +84,8 @@ class _DramaGridSkeletonState extends State mainAxisExtent: itemExtent, ), itemCount: _fillCount(height, itemExtent), - itemBuilder: (context, index) => _SkeletonCard(index: index), + itemBuilder: (context, index) => + _SkeletonCard(index: index, shimmerValue: shimmerValue), ); } @@ -105,48 +105,54 @@ class _DramaGridSkeletonState extends State final rows = (height / rowHeight).ceil() + 1; return math.max(widget.itemCount, rows * widget.columns); } - - Shader _shader(Rect bounds) { - final shift = _shimmerStart + _shimmerTravel * _shimmer.value; - return LinearGradient( - begin: Alignment(shift, -0.3), - end: Alignment(shift + 1, 0.3), - colors: const [ - AppColors.skeletonSurface, - _shimmerHighlight, - AppColors.skeletonSurface, - ], - ).createShader(bounds); - } } class _SkeletonCard extends StatelessWidget { - const _SkeletonCard({required this.index}); + const _SkeletonCard({required this.index, required this.shimmerValue}); final int index; + final double shimmerValue; @override Widget build(BuildContext context) { - return Column( - key: Key('drama-grid-skeleton-$index'), - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - AspectRatio( - aspectRatio: AppDimens.cardCoverAspectRatio, - child: const _SkeletonBlock(), - ), - const SizedBox(height: AppDimens.cardCoverTitleGap), - const _SkeletonBar(), - const SizedBox(height: AppDimens.skeletonTitleBarGap), - const FractionallySizedBox( - widthFactor: _subtitleWidthFactor, - child: _SkeletonBar(), - ), - ], + final phase = (shimmerValue + index * _shimmerPhaseStep) % 1.0; + return ShaderMask( + blendMode: BlendMode.srcATop, + shaderCallback: (bounds) => _cardShader(bounds, phase), + child: Column( + key: Key('drama-grid-skeleton-$index'), + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AspectRatio( + aspectRatio: AppDimens.cardCoverAspectRatio, + child: const _SkeletonBlock(), + ), + const SizedBox(height: AppDimens.cardCoverTitleGap), + const _SkeletonBar(), + const SizedBox(height: AppDimens.skeletonTitleBarGap), + const FractionallySizedBox( + widthFactor: _subtitleWidthFactor, + child: _SkeletonBar(), + ), + ], + ), ); } } +Shader _cardShader(Rect bounds, double shimmerValue) { + final shift = _shimmerStart + _shimmerTravel * shimmerValue; + return LinearGradient( + begin: Alignment(shift, -0.3), + end: Alignment(shift + 1, 0.3), + colors: const [ + AppColors.skeletonSurface, + _shimmerHighlight, + AppColors.skeletonSurface, + ], + ).createShader(bounds); +} + class _SkeletonBlock extends StatelessWidget { const _SkeletonBlock(); diff --git a/test/discover/drama_grid_skeleton_test.dart b/test/discover/drama_grid_skeleton_test.dart new file mode 100644 index 0000000..17256c6 --- /dev/null +++ b/test/discover/drama_grid_skeleton_test.dart @@ -0,0 +1,47 @@ +// Author: fruit.com.cn + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:fruitshorts/core/widgets/drama_grid_skeleton.dart'; + +void main() { + testWidgets('renders one local shimmer mask per skeleton card', ( + tester, + ) async { + tester.view.physicalSize = const Size(390, 1000); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: DramaGridSkeleton( + columns: 3, + horizontalPadding: 14, + crossAxisSpacing: 8, + mainAxisSpacing: 12, + ), + ), + ), + ); + await tester.pump(const Duration(milliseconds: 100)); + + expect(find.byType(GridView), findsOneWidget); + expect( + find.ancestor( + of: find.byType(GridView), + matching: find.byType(ShaderMask), + ), + findsNothing, + ); + for (var index = 0; index < 9; index++) { + final card = find.byKey(Key('drama-grid-skeleton-$index')); + expect(card, findsOneWidget); + expect( + find.ancestor(of: card, matching: find.byType(ShaderMask)), + findsOneWidget, + ); + } + }); +}