diff --git a/lib/core/widgets/drama_grid_skeleton.dart b/lib/core/widgets/drama_grid_skeleton.dart index 490bfda..a2deaf6 100644 --- a/lib/core/widgets/drama_grid_skeleton.dart +++ b/lib/core/widgets/drama_grid_skeleton.dart @@ -12,7 +12,7 @@ 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); +const Color _shimmerHighlight = Color(0x0DFFFFFF); /// 首页短剧网格的占位。行数至少铺满可视高度,并带一道扫光。 class DramaGridSkeleton extends StatefulWidget { @@ -116,41 +116,48 @@ class _SkeletonCard extends StatelessWidget { @override Widget build(BuildContext context) { 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(), + return Stack( + key: Key('drama-grid-skeleton-$index'), + children: [ + Column( + 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(), + ), + ], + ), + Positioned.fill( + child: IgnorePointer( + child: ClipRRect( + borderRadius: BorderRadius.circular(AppDimens.homeCardRadius), + child: DecoratedBox( + decoration: BoxDecoration(gradient: _cardHighlight(phase)), + ), + ), ), - const SizedBox(height: AppDimens.cardCoverTitleGap), - const _SkeletonBar(), - const SizedBox(height: AppDimens.skeletonTitleBarGap), - const FractionallySizedBox( - widthFactor: _subtitleWidthFactor, - child: _SkeletonBar(), - ), - ], - ), + ), + ], ); } } -Shader _cardShader(Rect bounds, double shimmerValue) { +LinearGradient _cardHighlight(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); + colors: const [Colors.transparent, _shimmerHighlight, Colors.transparent], + stops: const [0.42, 0.5, 0.58], + ); } class _SkeletonBlock extends StatelessWidget { diff --git a/test/discover/drama_grid_skeleton_test.dart b/test/discover/drama_grid_skeleton_test.dart index 17256c6..5b0431f 100644 --- a/test/discover/drama_grid_skeleton_test.dart +++ b/test/discover/drama_grid_skeleton_test.dart @@ -5,7 +5,7 @@ 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', ( + testWidgets('renders one narrow highlight overlay per skeleton card', ( tester, ) async { tester.view.physicalSize = const Size(390, 1000); @@ -28,20 +28,34 @@ void main() { await tester.pump(const Duration(milliseconds: 100)); expect(find.byType(GridView), findsOneWidget); - expect( - find.ancestor( - of: find.byType(GridView), - matching: find.byType(ShaderMask), - ), - findsNothing, - ); + expect(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)), + find.descendant(of: card, matching: find.byType(IgnorePointer)), findsOneWidget, ); + final overlay = find.descendant( + of: card, + matching: find.byWidgetPredicate( + (widget) => + widget is DecoratedBox && + widget.decoration is BoxDecoration && + (widget.decoration as BoxDecoration).gradient != null, + ), + ); + expect(overlay, findsOneWidget); + final decoration = + tester.widget(overlay).decoration as BoxDecoration; + final gradient = decoration.gradient! as LinearGradient; + expect(gradient.colors, const [ + Colors.transparent, + Color(0x0DFFFFFF), + Colors.transparent, + ]); + expect(gradient.stops, const [0.42, 0.5, 0.58]); + expect(gradient.stops![2] - gradient.stops![0], lessThanOrEqualTo(0.25)); } }); }