refactor(media): reconstruct the media library, with more animation and better performance

This commit is contained in:
ZhuJHua
2025-01-27 15:39:34 +08:00
parent be911fbd17
commit 5f62a228f0
17 changed files with 215 additions and 124 deletions

View File

@@ -0,0 +1,66 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:moodiary/utils/media_util.dart';
class ThumbnailImage extends StatelessWidget {
final String imagePath;
final int size;
final BoxFit? fit;
final Function() onTap;
const ThumbnailImage({
super.key,
required this.imagePath,
required this.size,
this.fit,
required this.onTap,
});
@override
Widget build(BuildContext context) {
final fileImage = FileImage(File(imagePath));
final devicePixelRatio = MediaQuery.devicePixelRatioOf(context);
final colorScheme = Theme.of(context).colorScheme;
final Future getAspectRatio = MediaUtil.getImageAspectRatio(fileImage);
return FutureBuilder(
future: getAspectRatio,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
final aspectRatio = snapshot.data as double;
return GestureDetector(
onTap: onTap,
child: Hero(
tag: imagePath,
child: Container(
decoration: BoxDecoration(
image: DecorationImage(
image: ResizeImage(
fileImage,
width: aspectRatio < 1.0
? size * devicePixelRatio.toInt()
: null,
height: aspectRatio >= 1.0
? (size * devicePixelRatio).toInt()
: null,
),
fit: fit ?? BoxFit.cover,
),
),
),
),
);
} else {
return Container(
color: colorScheme.surfaceContainer,
child: const Center(
child: Icon(
Icons.image_search_rounded,
),
),
);
}
},
);
}
}

View File

@@ -2,7 +2,9 @@ import 'package:flutter/material.dart';
import 'package:rive_animated_icon/rive_animated_icon.dart';
class Processing extends StatelessWidget {
const Processing({super.key});
final Color? color;
const Processing({super.key, this.color});
@override
Widget build(BuildContext context) {
@@ -11,7 +13,7 @@ class Processing extends StatelessWidget {
riveIcon: RiveIcon.reload,
width: 80,
height: 80,
color: colorScheme.onSurface,
color: color ?? colorScheme.onSurface,
strokeWidth: 4.0,
);
}

View File

@@ -1,3 +1,4 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'parser.dart';
@@ -639,22 +640,28 @@ class MarkdownToolbarState extends State<MarkdownToolbar> {
widget.useIncludedTextField
? _includedFocusNode.requestFocus()
: widget.focusNode?.requestFocus();
Format.toolbarItemPressed(
markdownToolbarOption: markdownToolbarOption,
controller: widget.useIncludedTextField
? _includedController
: widget.controller ?? _includedController,
selection: widget.useIncludedTextField
? _includedController.selection
: widget.controller?.selection ?? _includedController.selection,
option: option,
customBoldCharacter: widget.boldCharacter,
customItalicCharacter: widget.italicCharacter,
customCodeCharacter: widget.codeCharacter,
customBulletedListCharacter: widget.bulletedListCharacter,
customHorizontalRuleCharacter: widget.horizontalRuleCharacter,
mediaPath: mediaPath,
);
try {
Format.toolbarItemPressed(
markdownToolbarOption: markdownToolbarOption,
controller: widget.useIncludedTextField
? _includedController
: widget.controller ?? _includedController,
selection: widget.useIncludedTextField
? _includedController.selection
: widget.controller?.selection ?? _includedController.selection,
option: option,
customBoldCharacter: widget.boldCharacter,
customItalicCharacter: widget.italicCharacter,
customCodeCharacter: widget.codeCharacter,
customBulletedListCharacter: widget.bulletedListCharacter,
customHorizontalRuleCharacter: widget.horizontalRuleCharacter,
mediaPath: mediaPath,
);
} catch (e) {
if (kDebugMode) {
print(e);
}
}
}
@override

View File

@@ -18,7 +18,7 @@ class MediaAudioComponent extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
padding: const EdgeInsets.all(4.0),
child: Text(
DateFormat.yMMMEd().format(dateTime),
style: textStyle.titleSmall?.copyWith(color: colorScheme.secondary),

View File

@@ -1,8 +1,6 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:moodiary/common/values/border.dart';
import 'package:moodiary/components/base/image.dart';
import 'package:moodiary/router/app_routes.dart';
import 'package:refreshed/refreshed.dart';
@@ -19,7 +17,6 @@ class MediaImageComponent extends StatelessWidget {
@override
Widget build(BuildContext context) {
final pixelRatio = MediaQuery.devicePixelRatioOf(context);
final textStyle = Theme.of(context).textTheme;
final colorScheme = Theme.of(context).colorScheme;
@@ -28,9 +25,9 @@ class MediaImageComponent extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
padding: const EdgeInsets.all(4.0),
child: Text(
DateFormat.yMMMEd().format(dateTime),
DateFormat.yMMMMEEEEd().format(dateTime),
style: textStyle.titleSmall?.copyWith(color: colorScheme.secondary),
),
),
@@ -38,27 +35,19 @@ class MediaImageComponent extends StatelessWidget {
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 120,
childAspectRatio: 1.0,
crossAxisSpacing: 1.0,
mainAxisSpacing: 1.0,
),
physics: const NeverScrollableScrollPhysics(),
padding: EdgeInsets.zero,
padding: const EdgeInsets.all(4.0),
shrinkWrap: true,
itemBuilder: (context, index) {
return InkWell(
borderRadius: AppBorderRadius.mediumBorderRadius,
return ThumbnailImage(
imagePath: imageList[index],
size: 120,
onTap: () {
_toPhotoView(index, imageList);
},
child: Hero(
tag: imageList[index],
child: Card(
clipBehavior: Clip.hardEdge,
child: Image.file(
File(imageList[index]),
fit: BoxFit.cover,
cacheWidth: 120 * pixelRatio.toInt(),
),
),
),
);
},
itemCount: imageList.length,

View File

@@ -1,9 +1,7 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:intl/intl.dart';
import 'package:moodiary/common/values/border.dart';
import 'package:moodiary/components/base/image.dart';
import 'package:moodiary/router/app_routes.dart';
import 'package:path/path.dart';
import 'package:refreshed/refreshed.dart';
@@ -22,7 +20,6 @@ class MediaVideoComponent extends StatelessWidget {
@override
Widget build(BuildContext context) {
final pixelRatio = MediaQuery.devicePixelRatioOf(context);
final textStyle = Theme.of(context).textTheme;
final colorScheme = Theme.of(context).colorScheme;
// 将视频路径转换为缩略图路径
@@ -36,7 +33,7 @@ class MediaVideoComponent extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
padding: const EdgeInsets.all(4.0),
child: Text(
DateFormat.yMMMEd().format(dateTime),
style: textStyle.titleSmall?.copyWith(color: colorScheme.secondary),
@@ -44,34 +41,26 @@ class MediaVideoComponent extends StatelessWidget {
),
GridView.builder(
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 120,
childAspectRatio: 1.0,
),
maxCrossAxisExtent: 120,
childAspectRatio: 1.0,
crossAxisSpacing: 1.0,
mainAxisSpacing: 1.0),
physics: const NeverScrollableScrollPhysics(),
padding: EdgeInsets.zero,
padding: const EdgeInsets.all(4.0),
shrinkWrap: true,
itemBuilder: (context, index) {
return InkWell(
borderRadius: AppBorderRadius.mediumBorderRadius,
onTap: () {
_toVideoView(videoList, index);
},
child: Stack(
alignment: Alignment.center,
children: [
Positioned.fill(
child: Card(
clipBehavior: Clip.hardEdge,
child: Image.file(
File(thumbnailList[index]),
fit: BoxFit.cover,
cacheWidth: 120 * pixelRatio.toInt(),
),
),
),
const FaIcon(FontAwesomeIcons.play)
],
),
return Stack(
alignment: Alignment.center,
children: [
ThumbnailImage(
imagePath: thumbnailList[index],
size: 120,
onTap: () {
_toVideoView(videoList, index);
},
),
const FaIcon(FontAwesomeIcons.play)
],
);
},
itemCount: thumbnailList.length,

View File

@@ -41,9 +41,11 @@ class DiaryDetailsLogic extends GetxController {
}
//点击图片跳转到图片预览页面
void toPhotoView(List<String> imagePathList, int index) {
void toPhotoView(
List<String> imagePathList, int index, BuildContext context) {
HapticFeedback.selectionClick();
Get.toNamed(AppRoutes.photoPage, arguments: [imagePathList, index]);
// showImagePreview(context, imagePathList, index);
}
//点击视频跳转到视频预览页面

View File

@@ -256,6 +256,7 @@ class DiaryDetailsPage extends StatelessWidget {
'image', state.diary.imageName[i]);
}),
index,
context,
);
},
child: Image.file(

View File

@@ -306,9 +306,9 @@ class EditLogic extends GetxController {
}
//预览图片
void toPhotoView(List<String> imagePath, int index) {
Get.toNamed(AppRoutes.photoPage, arguments: [imagePath, index]);
}
// void toPhotoView(List<String> imagePath, int index) {
// Get.toNamed(AppRoutes.photoPage, arguments: [imagePath, index]);
// }
//预览视频
void toVideoView(List<String> videoPath, int index) {

View File

@@ -36,7 +36,6 @@ class DiaryLogic extends GetxController with GetTickerProviderStateMixin {
@override
void onReady() {
getHitokoto();
//监听 tab
tabController.addListener(_tabBarListener);
//监听 inner
@@ -79,7 +78,7 @@ class DiaryLogic extends GetxController with GetTickerProviderStateMixin {
checkPageChange();
// 检查是否显示顶部内容
_checkShowTop();
//homeLogic.resetNavigatorBar();
homeLogic.resetNavigatorBar();
}
/// 跳转到指定分类

View File

@@ -31,7 +31,7 @@ class HomeLogic extends GetxController with GetTickerProviderStateMixin {
CurvedAnimation(
parent: _barAnimationController, curve: Curves.easeInOut));
//late final PageController pageController = PageController();
late final PageController pageController = PageController();
late final FrostedGlassOverlayLogic frostedGlassOverlayLogic =
Bind.find<FrostedGlassOverlayLogic>();
@@ -58,7 +58,7 @@ class HomeLogic extends GetxController with GetTickerProviderStateMixin {
void onClose() {
_fabAnimationController.dispose();
_barAnimationController.dispose();
//pageController.dispose();
pageController.dispose();
_appLifecycleListener.dispose();
super.onClose();
}
@@ -100,7 +100,7 @@ class HomeLogic extends GetxController with GetTickerProviderStateMixin {
void changeNavigator(int index) {
navigatorIndex.value = index;
shouldShow.value = index == 0;
//pageController.jumpToPage(index);
pageController.jumpToPage(index);
}
Future<void> hideNavigatorBar() async {

View File

@@ -109,23 +109,17 @@ class HomePage extends StatelessWidget {
key: const ValueKey('body'),
builder: (_) {
return AdaptiveBackground(
child: Obx(
() {
return AnimatedSwitcher(
duration: const Duration(milliseconds: 150),
reverseDuration:
const Duration(milliseconds: 100),
key: logic.bodyKey,
child: switch (logic.navigatorIndex.value) {
0 => const DiaryPage(),
1 => const CalendarPage(),
2 => const MediaPage(),
3 => const SettingPage(),
_ => throw UnimplementedError(),
});
},
),
);
child: PageView(
key: logic.bodyKey,
controller: logic.pageController,
physics: const NeverScrollableScrollPhysics(),
children: const [
DiaryPage(),
CalendarPage(),
MediaPage(),
SettingPage(),
],
));
},
)
},

View File

@@ -126,7 +126,9 @@ class MediaPage extends StatelessWidget {
state.datetimeMediaMap[datetime]!;
return switch (state.mediaType.value) {
MediaType.image => MediaImageComponent(
dateTime: datetime, imageList: fileList),
dateTime: datetime,
imageList: fileList,
),
MediaType.audio => MediaAudioComponent(
dateTime: datetime, audioList: fileList),
MediaType.video => MediaVideoComponent(

View File

@@ -5,8 +5,13 @@ import 'image_state.dart';
class ImageLogic extends GetxController {
final ImageState state = ImageState();
late final PageController pageController =
PageController(initialPage: state.imageIndex.value);
late final PageController pageController;
@override
void onInit() {
pageController = PageController(initialPage: state.imageIndex.value);
super.onInit();
}
@override
void onClose() {

View File

@@ -3,12 +3,12 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:moodiary/common/values/media_type.dart';
import 'package:moodiary/components/base/button.dart';
import 'package:moodiary/components/loading/loading.dart';
import 'package:moodiary/pages/image/image_logic.dart';
import 'package:moodiary/utils/media_util.dart';
import 'package:photo_view/photo_view.dart';
import 'package:photo_view/photo_view_gallery.dart';
import 'package:refreshed/refreshed.dart';
import 'image_logic.dart';
import 'package:refreshed/get_state_manager/get_state_manager.dart';
class ImagePage extends StatelessWidget {
const ImagePage({super.key});
@@ -24,15 +24,15 @@ class ImagePage extends StatelessWidget {
extendBodyBehindAppBar: true,
appBar: AppBar(
backgroundColor: colorScheme.scrim.withAlpha((255 * 0.6).toInt()),
title: Obx(() {
return Visibility(
visible: state.imagePathList.length > 1,
child: Text(
title: Visibility(
visible: state.imagePathList.length > 1,
child: Obx(() {
return Text(
'${state.imageIndex.value + 1}/${state.imagePathList.length}',
style: const TextStyle(color: Colors.white),
),
);
}),
);
}),
),
iconTheme: const IconThemeData(color: Colors.white),
actions: [
IconButton(
@@ -49,7 +49,12 @@ class ImagePage extends StatelessWidget {
children: [
PhotoViewGallery.builder(
scrollPhysics: const PageScrollPhysics(),
backgroundDecoration: const BoxDecoration(
color: Colors.black,
),
pageController: logic.pageController,
wantKeepAlive: true,
gaplessPlayback: true,
builder: (BuildContext context, int index) {
return PhotoViewGalleryPageOptions(
imageProvider: FileImage(File(state.imagePathList[index])),
@@ -60,8 +65,14 @@ class ImagePage extends StatelessWidget {
},
itemCount: state.imagePathList.length,
onPageChanged: logic.changePage,
loadingBuilder: (context, event) =>
const Center(child: CircularProgressIndicator()),
loadingBuilder: (context, event) {
return Container(
color: Colors.black,
child: const Center(
child: Processing(color: Colors.white),
),
);
},
),
Obx(() {
return Visibility(

View File

@@ -58,7 +58,7 @@ class WebViewPage extends StatelessWidget {
child: InAppWebView(
initialUrlRequest: URLRequest(url: WebUri(state.url)),
initialSettings: logic.webSettings,
// pullToRefreshController: logic.pullToRefreshController,
// pullToRefreshController: logic.pullToRefreshController,
onWebViewCreated: (controller) {
logic.webViewController = controller;
},

View File

@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:collection';
import 'dart:io';
import 'package:fc_native_video_thumbnail/fc_native_video_thumbnail.dart';
@@ -185,24 +186,47 @@ class MediaUtil {
return completer.future;
}
//获取图片宽高比例
static const int _maxCacheSize = 1000;
static final LinkedHashMap<String, double> _cache = LinkedHashMap();
static Future<double> getImageAspectRatio(ImageProvider imageProvider) async {
final Completer<double> completer = Completer<double>();
final ImageStream stream =
imageProvider.resolve(const ImageConfiguration());
stream.addListener(
ImageStreamListener(
(ImageInfo info, bool _) {
final double aspectRatio = double.parse(
(info.image.width.toDouble() / info.image.height.toDouble())
.toStringAsPrecision(2));
completer.complete(aspectRatio);
},
),
final key = _getImageKey(imageProvider);
if (_cache.containsKey(key)) {
return _cache[key]!;
}
final completer = Completer<double>();
final imageStream = imageProvider.resolve(const ImageConfiguration());
imageStream.addListener(
ImageStreamListener((ImageInfo info, bool _) {
final aspectRatio = info.image.width / info.image.height;
_addToCache(key, aspectRatio);
completer.complete(aspectRatio);
}),
);
return completer.future;
}
static void _addToCache(String key, double ratio) {
if (_cache.length >= _maxCacheSize) {
_cache.remove(_cache.keys.first);
}
_cache[key] = ratio;
}
static String _getImageKey(ImageProvider imageProvider) {
if (imageProvider is AssetImage) {
return 'asset_${imageProvider.assetName}';
} else if (imageProvider is NetworkImage) {
return 'network_${imageProvider.url}';
} else if (imageProvider is FileImage) {
return 'file_${imageProvider.file.path}';
}
return imageProvider.toString();
}
//获取单个图片,拍照或者相册
static Future<XFile?> pickPhoto(ImageSource imageSource) async {
return await _picker.pickImage(