fix(ui): 骨架屏改为逐卡片扫光
This commit is contained in:
1 parent
00405d2e3b
commit
a5dbae7fff
2 files changed
+91
-38
No files matched your search
@@ -11,6 +11,7 @@ const Duration _shimmerPeriod = Duration(milliseconds: 1400);
|
|||||||
const double _shimmerTravel = 2.4;
|
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 Color _shimmerHighlight = Color(0x12FFFFFF);
|
const Color _shimmerHighlight = Color(0x12FFFFFF);
|
||||||
|
|
||||||
/// 首页短剧网格的占位。行数至少铺满可视高度,并带一道扫光。
|
/// 首页短剧网格的占位。行数至少铺满可视高度,并带一道扫光。
|
||||||
@@ -53,21 +54,19 @@ class _DramaGridSkeletonState extends State<DramaGridSkeleton>
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return LayoutBuilder(
|
return LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
final grid = _grid(constraints.maxWidth, constraints.maxHeight);
|
|
||||||
return AnimatedBuilder(
|
return AnimatedBuilder(
|
||||||
animation: _shimmer,
|
animation: _shimmer,
|
||||||
builder: (context, child) => ShaderMask(
|
builder: (context, _) => _grid(
|
||||||
blendMode: BlendMode.srcATop,
|
constraints.maxWidth,
|
||||||
shaderCallback: (bounds) => _shader(bounds),
|
constraints.maxHeight,
|
||||||
child: child,
|
_shimmer.value,
|
||||||
),
|
),
|
||||||
child: grid,
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _grid(double width, double height) {
|
Widget _grid(double width, double height, double shimmerValue) {
|
||||||
final itemWidth = _itemWidth(width);
|
final itemWidth = _itemWidth(width);
|
||||||
final itemExtent = _itemExtent(itemWidth);
|
final itemExtent = _itemExtent(itemWidth);
|
||||||
return GridView.builder(
|
return GridView.builder(
|
||||||
@@ -85,7 +84,8 @@ class _DramaGridSkeletonState extends State<DramaGridSkeleton>
|
|||||||
mainAxisExtent: itemExtent,
|
mainAxisExtent: itemExtent,
|
||||||
),
|
),
|
||||||
itemCount: _fillCount(height, 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<DramaGridSkeleton>
|
|||||||
final rows = (height / rowHeight).ceil() + 1;
|
final rows = (height / rowHeight).ceil() + 1;
|
||||||
return math.max(widget.itemCount, rows * widget.columns);
|
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 {
|
class _SkeletonCard extends StatelessWidget {
|
||||||
const _SkeletonCard({required this.index});
|
const _SkeletonCard({required this.index, required this.shimmerValue});
|
||||||
|
|
||||||
final int index;
|
final int index;
|
||||||
|
final double shimmerValue;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(
|
final phase = (shimmerValue + index * _shimmerPhaseStep) % 1.0;
|
||||||
key: Key('drama-grid-skeleton-$index'),
|
return ShaderMask(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
blendMode: BlendMode.srcATop,
|
||||||
children: [
|
shaderCallback: (bounds) => _cardShader(bounds, phase),
|
||||||
AspectRatio(
|
child: Column(
|
||||||
aspectRatio: AppDimens.cardCoverAspectRatio,
|
key: Key('drama-grid-skeleton-$index'),
|
||||||
child: const _SkeletonBlock(),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
),
|
children: [
|
||||||
const SizedBox(height: AppDimens.cardCoverTitleGap),
|
AspectRatio(
|
||||||
const _SkeletonBar(),
|
aspectRatio: AppDimens.cardCoverAspectRatio,
|
||||||
const SizedBox(height: AppDimens.skeletonTitleBarGap),
|
child: const _SkeletonBlock(),
|
||||||
const FractionallySizedBox(
|
),
|
||||||
widthFactor: _subtitleWidthFactor,
|
const SizedBox(height: AppDimens.cardCoverTitleGap),
|
||||||
child: _SkeletonBar(),
|
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 {
|
class _SkeletonBlock extends StatelessWidget {
|
||||||
const _SkeletonBlock();
|
const _SkeletonBlock();
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in new issue
Block a user