diff --git a/lib/common/skeleton/skeleton.dart b/lib/common/skeleton/skeleton.dart new file mode 100644 index 00000000..34e87f55 --- /dev/null +++ b/lib/common/skeleton/skeleton.dart @@ -0,0 +1,191 @@ +import 'package:flutter/material.dart'; + +class Skeleton extends StatelessWidget { + final Widget child; + + const Skeleton({ + required this.child, + super.key, + }); + + @override + Widget build(BuildContext context) { + var shimmerGradient = LinearGradient( + colors: [ + Colors.transparent, + Theme.of(context).colorScheme.background.withAlpha(10), + Theme.of(context).colorScheme.background.withAlpha(10), + Colors.transparent, + ], + stops: const [ + 0.1, + 0.3, + 0.5, + 0.7, + ], + begin: const Alignment(-1.0, -0.3), + end: const Alignment(1.0, 0.9), + tileMode: TileMode.clamp, + ); + return Shimmer( + linearGradient: shimmerGradient, + child: ShimmerLoading( + isLoading: true, + child: child, + ), + ); + } +} + +class Shimmer extends StatefulWidget { + static ShimmerState? of(BuildContext context) { + return context.findAncestorStateOfType(); + } + + const Shimmer({ + super.key, + required this.linearGradient, + this.child, + }); + + final LinearGradient linearGradient; + final Widget? child; + + @override + ShimmerState createState() => ShimmerState(); +} + +class ShimmerState extends State with SingleTickerProviderStateMixin { + late AnimationController _shimmerController; + + @override + void initState() { + super.initState(); + + _shimmerController = AnimationController.unbounded(vsync: this) + ..repeat(min: -0.5, max: 1.5, period: const Duration(milliseconds: 1000)); + } + + @override + void dispose() { + _shimmerController.dispose(); + super.dispose(); + } + + LinearGradient get gradient => LinearGradient( + colors: widget.linearGradient.colors, + stops: widget.linearGradient.stops, + begin: widget.linearGradient.begin, + end: widget.linearGradient.end, + transform: _SlidingGradientTransform( + slidePercent: _shimmerController.value, + ), + ); + + bool get isSized => + (context.findRenderObject() as RenderBox?)?.hasSize ?? false; + + Size get size => (context.findRenderObject() as RenderBox).size; + + Offset getDescendantOffset({ + required RenderBox descendant, + Offset offset = Offset.zero, + }) { + final shimmerBox = context.findRenderObject() as RenderBox; + return descendant.localToGlobal(offset, ancestor: shimmerBox); + } + + Listenable get shimmerChanges => _shimmerController; + + @override + Widget build(BuildContext context) { + return widget.child ?? const SizedBox(); + } +} + +class _SlidingGradientTransform extends GradientTransform { + const _SlidingGradientTransform({ + required this.slidePercent, + }); + + final double slidePercent; + + @override + Matrix4? transform(Rect bounds, {TextDirection? textDirection}) { + return Matrix4.translationValues(bounds.width * slidePercent, 0.0, 0.0); + } +} + +class ShimmerLoading extends StatefulWidget { + const ShimmerLoading({ + super.key, + required this.isLoading, + required this.child, + }); + + final bool isLoading; + final Widget child; + + @override + State createState() => _ShimmerLoadingState(); +} + +class _ShimmerLoadingState extends State { + Listenable? _shimmerChanges; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + if (_shimmerChanges != null) { + _shimmerChanges!.removeListener(_onShimmerChange); + } + _shimmerChanges = Shimmer.of(context)?.shimmerChanges; + if (_shimmerChanges != null) { + _shimmerChanges!.addListener(_onShimmerChange); + } + } + + @override + void dispose() { + _shimmerChanges?.removeListener(_onShimmerChange); + super.dispose(); + } + + void _onShimmerChange() { + if (widget.isLoading) { + setState(() {}); + } + } + + @override + Widget build(BuildContext context) { + if (!widget.isLoading) { + return widget.child; + } + + final shimmer = Shimmer.of(context)!; + if (!shimmer.isSized) { + return const SizedBox(); + } + final shimmerSize = shimmer.size; + final gradient = shimmer.gradient; + final offsetWithinShimmer = shimmer.getDescendantOffset( + descendant: context.findRenderObject() as RenderBox, + ); + + return ShaderMask( + blendMode: BlendMode.srcATop, + shaderCallback: (bounds) { + return gradient.createShader( + Rect.fromLTWH( + -offsetWithinShimmer.dx, + -offsetWithinShimmer.dy, + shimmerSize.width, + shimmerSize.height, + ), + ); + }, + child: widget.child, + ); + } +} diff --git a/lib/common/skeleton/video_card_v.dart b/lib/common/skeleton/video_card_v.dart new file mode 100644 index 00000000..1c9ef23d --- /dev/null +++ b/lib/common/skeleton/video_card_v.dart @@ -0,0 +1,74 @@ +import 'package:pilipala/common/constants.dart'; +import 'package:flutter/material.dart'; +import 'skeleton.dart'; + +class VideoCardVSkeleton extends StatelessWidget { + const VideoCardVSkeleton({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Skeleton( + child: Card( + elevation: 0.8, + shape: RoundedRectangleBorder( + borderRadius: StyleString.mdRadius, + ), + margin: EdgeInsets.zero, + child: Column( + children: [ + AspectRatio( + aspectRatio: StyleString.aspectRatio, + child: LayoutBuilder( + builder: (context, boxConstraints) { + return Container( + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background, + borderRadius: BorderRadius.circular(6), + border: Border.all( + color: Theme.of(context) + .colorScheme + .outline + .withOpacity(0.1), + ), + ), + ); + }, + ), + ), + Padding( + // 多列 + padding: const EdgeInsets.fromLTRB(8, 8, 6, 7), + // 单列 + // padding: const EdgeInsets.fromLTRB(14, 10, 4, 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + // const SizedBox(height: 6), + Container( + width: 200, + height: 13, + color: Theme.of(context).colorScheme.background, + ), + const SizedBox(height: 5), + Container( + width: 150, + height: 13, + color: Theme.of(context).colorScheme.background, + ), + const SizedBox(height: 12), + Container( + width: 80, + height: 13, + color: Theme.of(context).colorScheme.background, + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/home/controller.dart b/lib/pages/home/controller.dart index 961d05c0..a8c5b679 100644 --- a/lib/pages/home/controller.dart +++ b/lib/pages/home/controller.dart @@ -26,7 +26,6 @@ class HomeController extends GetxController { ); List list = []; for (var i in res.data['data']['item']) { - print(i); list.add(RecVideoItemModel.fromJson(i)); } if (type == 'init') { diff --git a/lib/pages/home/view.dart b/lib/pages/home/view.dart index 2482e465..fba2f82c 100644 --- a/lib/pages/home/view.dart +++ b/lib/pages/home/view.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:pilipala/common/skeleton/video_card_v.dart'; import 'package:pilipala/common/widgets/video_card_v.dart'; import './controller.dart'; import 'package:pilipala/common/constants.dart'; @@ -84,7 +85,7 @@ class _HomePageState extends State (BuildContext context, int index) { return videoList.isNotEmpty ? VideoCardV(videoItem: videoList[index]) - : const Text('加载中'); + : const VideoCardVSkeleton(); }, childCount: videoList.isNotEmpty ? videoList.length : 10, ),