fix(ui): 骨架屏扫光收窄压淡

This commit is contained in:
Zhou Weidong committed 2026-10-27 11:12:31 +08:00
1 parent a5dbae7fff
commit bd826a9236
2 files changed
+57 -36

No files matched your search

+34 -27
View File
@@ -12,7 +12,7 @@ const double _shimmerTravel = 2.4;
const double _shimmerStart = -1.2; const double _shimmerStart = -1.2;
const double _subtitleWidthFactor = 0.62; const double _subtitleWidthFactor = 0.62;
const double _shimmerPhaseStep = 0.13; const double _shimmerPhaseStep = 0.13;
const Color _shimmerHighlight = Color(0x12FFFFFF); const Color _shimmerHighlight = Color(0x0DFFFFFF);
/// 首页短剧网格的占位。行数至少铺满可视高度,并带一道扫光。 /// 首页短剧网格的占位。行数至少铺满可视高度,并带一道扫光。
class DramaGridSkeleton extends StatefulWidget { class DramaGridSkeleton extends StatefulWidget {
@@ -116,41 +116,48 @@ class _SkeletonCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final phase = (shimmerValue + index * _shimmerPhaseStep) % 1.0; final phase = (shimmerValue + index * _shimmerPhaseStep) % 1.0;
return ShaderMask( return Stack(
blendMode: BlendMode.srcATop, key: Key('drama-grid-skeleton-$index'),
shaderCallback: (bounds) => _cardShader(bounds, phase), children: [
child: Column( Column(
key: Key('drama-grid-skeleton-$index'), crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, children: [
children: [ AspectRatio(
AspectRatio( aspectRatio: AppDimens.cardCoverAspectRatio,
aspectRatio: AppDimens.cardCoverAspectRatio, child: const _SkeletonBlock(),
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; final shift = _shimmerStart + _shimmerTravel * shimmerValue;
return LinearGradient( return LinearGradient(
begin: Alignment(shift, -0.3), begin: Alignment(shift, -0.3),
end: Alignment(shift + 1, 0.3), end: Alignment(shift + 1, 0.3),
colors: const [ colors: const [Colors.transparent, _shimmerHighlight, Colors.transparent],
AppColors.skeletonSurface, stops: const [0.42, 0.5, 0.58],
_shimmerHighlight, );
AppColors.skeletonSurface,
],
).createShader(bounds);
} }
class _SkeletonBlock extends StatelessWidget { class _SkeletonBlock extends StatelessWidget {
+23 -9
View File
@@ -5,7 +5,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:fruitshorts/core/widgets/drama_grid_skeleton.dart'; import 'package:fruitshorts/core/widgets/drama_grid_skeleton.dart';
void main() { void main() {
testWidgets('renders one local shimmer mask per skeleton card', ( testWidgets('renders one narrow highlight overlay per skeleton card', (
tester, tester,
) async { ) async {
tester.view.physicalSize = const Size(390, 1000); tester.view.physicalSize = const Size(390, 1000);
@@ -28,20 +28,34 @@ void main() {
await tester.pump(const Duration(milliseconds: 100)); await tester.pump(const Duration(milliseconds: 100));
expect(find.byType(GridView), findsOneWidget); expect(find.byType(GridView), findsOneWidget);
expect( expect(find.byType(ShaderMask), findsNothing);
find.ancestor(
of: find.byType(GridView),
matching: find.byType(ShaderMask),
),
findsNothing,
);
for (var index = 0; index < 9; index++) { for (var index = 0; index < 9; index++) {
final card = find.byKey(Key('drama-grid-skeleton-$index')); final card = find.byKey(Key('drama-grid-skeleton-$index'));
expect(card, findsOneWidget); expect(card, findsOneWidget);
expect( expect(
find.ancestor(of: card, matching: find.byType(ShaderMask)), find.descendant(of: card, matching: find.byType(IgnorePointer)),
findsOneWidget, 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<DecoratedBox>(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));
} }
}); });
} }