feat: 42kit Nav

feat: Animation Optimization
This commit is contained in:
2025-05-04 14:07:56 +08:00
parent a2de310d84
commit 03c941c970
23 changed files with 5618 additions and 493 deletions

View File

@ -1,5 +1,8 @@
import 'package:extended_image/extended_image.dart';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'cache_svg_image.dart';
class CacheNetImage extends StatelessWidget {
final String url;
@ -7,11 +10,18 @@ class CacheNetImage extends StatelessWidget {
final double? height;
final BoxFit? fit;
const CacheNetImage(
{super.key, required this.url, this.width, this.height, this.fit});
const CacheNetImage({super.key, required this.url, this.width, this.height, this.fit});
@override
Widget build(BuildContext context) {
if (url.endsWith(".svg")) {
return CachedSvgImage(
url,
width: width,
height: height,
fit: fit ?? BoxFit.contain,
);
}
return ExtendedImage.network(
url,
width: width,
@ -20,14 +30,11 @@ class CacheNetImage extends StatelessWidget {
loadStateChanged: (ExtendedImageState state) {
switch (state.extendedImageLoadState) {
case LoadState.loading:
return const Center(
child: Padding(
padding: EdgeInsets.all(8.0),
child: Column(
children: [
ProgressRing(),
],
),
return SizedBox(
width: width,
height: height,
child: Center(
child: ProgressRing(),
),
);
case LoadState.failed:

View File

@ -0,0 +1,64 @@
import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_svg/svg.dart';
import 'package:starcitizen_doctor/common/utils/file_cache_utils.dart';
class CachedSvgImage extends HookWidget {
final String url;
final double? width;
final double? height;
final BoxFit? fit;
const CachedSvgImage(this.url, {super.key, this.width, this.height, this.fit});
@override
Widget build(BuildContext context) {
final cachedFile = useState<File?>(null);
final errorInfo = useState<String?>(null);
useEffect(
() {
() async {
try {
cachedFile.value = await FileCacheUtils.getFile(url);
} catch (e) {
debugPrint("Error loading SVG: $e");
errorInfo.value = "Error loading SVG: $e";
}
}();
return null;
},
[url],
);
if (errorInfo.value != null) {
return SizedBox(
width: width,
height: height,
child: Center(
child: Text(
errorInfo.value!,
style: TextStyle(color: Colors.red),
),
),
);
}
return cachedFile.value != null
? SvgPicture.file(
cachedFile.value!,
width: width,
height: height,
fit: fit ?? BoxFit.contain,
)
: SizedBox(
width: width,
height: height,
child: Center(
child: ProgressRing(),
),
);
}
}

View File

@ -0,0 +1,71 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
class GridItemAnimator extends HookWidget {
final Widget child; // 要添加动画效果的子组件
final int index; // 条目在网格中的索引位置
final Duration duration; // 动画持续时间
final Duration delayPerItem; // 每个条目之间的延迟时间
final double slideOffset; // 上移的偏移量(像素)
const GridItemAnimator({
super.key,
required this.child,
required this.index,
this.duration = const Duration(milliseconds: 230),
this.delayPerItem = const Duration(milliseconds: 50),
this.slideOffset = 20.0,
});
@override
Widget build(BuildContext context) {
// 创建动画控制器
final animationController = useAnimationController(
duration: duration,
);
// 创建不透明度动画
final opacityAnimation = useAnimation(
Tween<double>(
begin: 0.0, // 开始时完全透明
end: 1.0, // 结束时完全不透明
).animate(CurvedAnimation(
parent: animationController,
curve: Curves.easeOut,
)),
);
// 创建位移动画
final slideAnimation = useAnimation(
Tween<double>(
begin: 1.0, // 开始位置
end: 0.0, // 结束位置
).animate(CurvedAnimation(
parent: animationController,
curve: Curves.easeOutCubic,
)),
);
// 组件挂载后启动动画
useEffect(() {
// 根据索引计算延迟时间,实现逐个条目入场
final delay = delayPerItem * index;
Future.delayed(delay, () {
if (animationController.status != AnimationStatus.completed) {
animationController.forward();
}
});
return null;
}, const []);
// 应用动画效果
return Opacity(
opacity: opacityAnimation,
child: Transform.translate(
offset: Offset(0, slideOffset * slideAnimation), // 向上平移动画
child: child,
),
);
}
}

View File

@ -15,6 +15,9 @@ import 'dart:ui' as ui;
export 'src/cache_image.dart';
export 'src/countdown_time_text.dart';
export 'src/cache_svg_image.dart';
export 'src/grid_item_animator.dart';
export '../common/utils/async.dart';
export '../common/utils/base_utils.dart';
export 'package:starcitizen_doctor/generated/l10n.dart';
@ -137,14 +140,13 @@ ColorFilter makeSvgColor(Color color) {
return ui.ColorFilter.mode(color, ui.BlendMode.srcIn);
}
CustomTransitionPage<T> myPageBuilder<T>(
BuildContext context, GoRouterState state, Widget child) {
CustomTransitionPage<T> myPageBuilder<T>(BuildContext context, GoRouterState state, Widget child) {
return CustomTransitionPage(
child: child,
transitionDuration: const Duration(milliseconds: 150),
reverseTransitionDuration: const Duration(milliseconds: 150),
transitionsBuilder: (BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) {
transitionsBuilder:
(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(0.0, 1.0),
@ -164,12 +166,7 @@ class LoadingWidget<T> extends HookConsumerWidget {
final Widget Function(BuildContext context, T data) childBuilder;
final Duration? autoRefreshDuration;
const LoadingWidget(
{super.key,
this.data,
required this.childBuilder,
this.onLoadData,
this.autoRefreshDuration});
const LoadingWidget({super.key, this.data, required this.childBuilder, this.onLoadData, this.autoRefreshDuration});
@override
Widget build(BuildContext context, WidgetRef ref) {
@ -204,8 +201,7 @@ class LoadingWidget<T> extends HookConsumerWidget {
return childBuilder(context, (data ?? dataState.value) as T);
}
void _loadData(
ValueNotifier<T?> dataState, ValueNotifier<String> errorMsg) async {
void _loadData(ValueNotifier<T?> dataState, ValueNotifier<String> errorMsg) async {
errorMsg.value = "";
try {
final r = await onLoadData!();