From e5578bdeaed435fd42fec21befd9b65cfd0d7e13 Mon Sep 17 00:00:00 2001 From: ZhuJHua <1624109111@qq.com> Date: Sun, 23 Feb 2025 02:17:17 +0800 Subject: [PATCH] feat: improve image handling with enhanced hero transitions and dynamic sizing --- lib/components/base/image.dart | 2 +- .../markdown_embed/image_embed.dart | 24 ++-- lib/components/media/media_image_view.dart | 83 +++++++++---- lib/components/media/media_video_view.dart | 31 +++-- lib/components/quill_embed/image_embed.dart | 37 +++--- lib/pages/image/image_view.dart | 109 +++++++++--------- lib/router/app_pages.dart | 9 ++ 7 files changed, 175 insertions(+), 120 deletions(-) diff --git a/lib/components/base/image.dart b/lib/components/base/image.dart index c26e0c2..6a52f1b 100644 --- a/lib/components/base/image.dart +++ b/lib/components/base/image.dart @@ -29,7 +29,7 @@ class ThumbnailImage extends StatelessWidget { key: ValueKey(imagePath), image: ResizeImage( imageProvider, - width: aspectRatio < 1.0 ? size * pixelRatio.toInt() : null, + width: aspectRatio < 1.0 ? (size * pixelRatio).toInt() : null, height: aspectRatio >= 1.0 ? (size * pixelRatio).toInt() : null, ), fit: fit ?? BoxFit.cover, diff --git a/lib/components/markdown_embed/image_embed.dart b/lib/components/markdown_embed/image_embed.dart index 3c6f2ec..f888da0 100644 --- a/lib/components/markdown_embed/image_embed.dart +++ b/lib/components/markdown_embed/image_embed.dart @@ -1,38 +1,36 @@ import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:moodiary/router/app_routes.dart'; +import 'package:moodiary/pages/image/image_view.dart'; import 'package:moodiary/utils/file_util.dart'; -import 'package:refreshed/refreshed.dart'; +import 'package:uuid/uuid.dart'; class MarkdownImageEmbed extends StatelessWidget { final bool isEdit; final String imageName; - const MarkdownImageEmbed( - {super.key, required this.isEdit, required this.imageName}); + const MarkdownImageEmbed({ + super.key, + required this.isEdit, + required this.imageName, + }); @override - Widget build( - BuildContext context, - ) { + Widget build(BuildContext context) { final imagePath = isEdit ? imageName : FileUtil.getRealPath('image', imageName); - + final heroPrefix = const Uuid().v4(); return Center( child: GestureDetector( onTap: () { if (!isEdit) { - Get.toNamed(AppRoutes.photoPage, arguments: [ - [imagePath], - 0, - ]); + showImageView(context, [imagePath], 0, heroTagPrefix: heroPrefix); } }, child: ConstrainedBox( constraints: const BoxConstraints(maxHeight: 300), child: Hero( - tag: imagePath, + tag: '${heroPrefix}0', child: Card.outlined( clipBehavior: Clip.hardEdge, color: Colors.transparent, diff --git a/lib/components/media/media_image_view.dart b/lib/components/media/media_image_view.dart index a71c6e6..f4d651c 100644 --- a/lib/components/media/media_image_view.dart +++ b/lib/components/media/media_image_view.dart @@ -1,7 +1,9 @@ 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/pages/image/image_view.dart'; +import 'package:uuid/uuid.dart'; class MediaImageComponent extends StatelessWidget { final DateTime dateTime; @@ -17,38 +19,73 @@ class MediaImageComponent extends StatelessWidget { Widget build(BuildContext context) { final textStyle = Theme.of(context).textTheme; final colorScheme = Theme.of(context).colorScheme; - + final heroPrefix = const Uuid().v4(); return Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Padding( - padding: const EdgeInsets.all(4.0), - child: Text( - DateFormat.yMMMMEEEEd().format(dateTime), - style: textStyle.titleSmall?.copyWith(color: colorScheme.secondary), + padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 4.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + DateFormat.yMMMMEEEEd().format(dateTime), + style: textStyle.titleSmall?.copyWith( + color: colorScheme.secondary, + ), + ), + Text( + '${imageList.length} ${imageList.length > 1 ? 'Photos' : 'Photo'}', + style: textStyle.labelMedium?.copyWith( + color: colorScheme.tertiary, + ), + ), + ], ), ), - GridView.builder( - gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 120, - childAspectRatio: 1.0, - crossAxisSpacing: 1.0, - mainAxisSpacing: 1.0, - ), - physics: const NeverScrollableScrollPhysics(), + Padding( padding: const EdgeInsets.all(4.0), - shrinkWrap: true, - itemBuilder: (context, index) { - return ThumbnailImage( - imagePath: imageList[index], - size: 120, - onTap: () async { - await showImageView(context, imageList, index); + child: ClipRRect( + borderRadius: AppBorderRadius.mediumBorderRadius, + child: GridView.builder( + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 120, + childAspectRatio: 1.0, + crossAxisSpacing: 1.5, + mainAxisSpacing: 1.5, + ), + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemBuilder: (context, index) { + final image = ThumbnailImage( + imagePath: imageList[index], + size: 120, + heroTag: '$heroPrefix$index', + onTap: () async { + await showImageView( + context, + imageList, + index, + heroTagPrefix: heroPrefix, + ); + }, + ); + return GestureDetector( + onTap: () async { + await showImageView( + context, + imageList, + index, + heroTagPrefix: heroPrefix, + ); + }, + child: image, + ); }, - ); - }, - itemCount: imageList.length, + itemCount: imageList.length, + ), + ), ), ], ); diff --git a/lib/components/media/media_video_view.dart b/lib/components/media/media_video_view.dart index da794a2..65adb05 100644 --- a/lib/components/media/media_video_view.dart +++ b/lib/components/media/media_video_view.dart @@ -5,13 +5,17 @@ import 'package:moodiary/components/base/image.dart'; import 'package:moodiary/router/app_routes.dart'; import 'package:path/path.dart'; import 'package:refreshed/refreshed.dart'; +import 'package:uuid/uuid.dart'; class MediaVideoComponent extends StatelessWidget { final DateTime dateTime; final List videoList; - const MediaVideoComponent( - {super.key, required this.dateTime, required this.videoList}); + const MediaVideoComponent({ + super.key, + required this.dateTime, + required this.videoList, + }); //点击视频跳转到视频预览 void _toVideoView(List videoPathList, int index) { @@ -23,11 +27,12 @@ class MediaVideoComponent extends StatelessWidget { final textStyle = Theme.of(context).textTheme; final colorScheme = Theme.of(context).colorScheme; // 将视频路径转换为缩略图路径 - final thumbnailList = videoList.map((e) { - final id = e.split('video-')[1].split('.')[0]; - return '${dirname(e)}/thumbnail-$id.jpeg'; - }).toList(); - + final thumbnailList = + videoList.map((e) { + final id = e.split('video-')[1].split('.')[0]; + return '${dirname(e)}/thumbnail-$id.jpeg'; + }).toList(); + final heroPrefix = const Uuid().v4(); return Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, @@ -41,10 +46,11 @@ class MediaVideoComponent extends StatelessWidget { ), GridView.builder( gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 120, - childAspectRatio: 1.0, - crossAxisSpacing: 1.0, - mainAxisSpacing: 1.0), + maxCrossAxisExtent: 120, + childAspectRatio: 1.0, + crossAxisSpacing: 1.0, + mainAxisSpacing: 1.0, + ), physics: const NeverScrollableScrollPhysics(), padding: const EdgeInsets.all(4.0), shrinkWrap: true, @@ -54,12 +60,13 @@ class MediaVideoComponent extends StatelessWidget { children: [ ThumbnailImage( imagePath: thumbnailList[index], + heroTag: '$heroPrefix$index', size: 120, onTap: () { _toVideoView(videoList, index); }, ), - const FaIcon(FontAwesomeIcons.play) + const FaIcon(FontAwesomeIcons.play), ], ); }, diff --git a/lib/components/quill_embed/image_embed.dart b/lib/components/quill_embed/image_embed.dart index d47142a..5df51ea 100644 --- a/lib/components/quill_embed/image_embed.dart +++ b/lib/components/quill_embed/image_embed.dart @@ -2,9 +2,9 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_quill/flutter_quill.dart'; -import 'package:moodiary/router/app_routes.dart'; +import 'package:moodiary/common/values/border.dart'; +import 'package:moodiary/pages/image/image_view.dart'; import 'package:moodiary/utils/file_util.dart'; -import 'package:refreshed/refreshed.dart'; class ImageBlockEmbed extends BlockEmbed { const ImageBlockEmbed(String value) : super(embedType, value); @@ -28,34 +28,35 @@ class ImageEmbedBuilder extends EmbedBuilder { String toPlainText(Embed node) => ''; @override - Widget build( - BuildContext context, - EmbedContext embedContext, - ) { + Widget build(BuildContext context, EmbedContext embedContext) { final imageEmbed = ImageBlockEmbed(embedContext.node.value.data); // 从数据构造 ImageBlockEmbed - final imagePath = isEdit - ? imageEmbed.name - : FileUtil.getRealPath('image', imageEmbed.name); - + final imagePath = + isEdit + ? imageEmbed.name + : FileUtil.getRealPath('image', imageEmbed.name); return Center( child: GestureDetector( onTap: () { if (!isEdit) { - Get.toNamed(AppRoutes.photoPage, arguments: [ + showImageView( + context, [imagePath], 0, - ]); + heroTagPrefix: imageEmbed.name, + ); } }, child: ConstrainedBox( constraints: const BoxConstraints(maxHeight: 300), - child: Hero( - tag: imagePath, - child: Card.outlined( - clipBehavior: Clip.hardEdge, - color: Colors.transparent, - child: Image.file(File(imagePath)), + child: Card.outlined( + color: Colors.transparent, + child: Hero( + tag: '${imageEmbed.name}0', + child: ClipRRect( + borderRadius: AppBorderRadius.mediumBorderRadius, + child: Image.file(File(imagePath)), + ), ), ), ), diff --git a/lib/pages/image/image_view.dart b/lib/pages/image/image_view.dart index 80dfbee..77a6d4c 100644 --- a/lib/pages/image/image_view.dart +++ b/lib/pages/image/image_view.dart @@ -139,64 +139,59 @@ class ImagePage extends StatelessWidget { tag: _tag, assignId: true, builder: (_) { - return GestureDetector( - onTap: () { - Navigator.pop(context); - }, - child: Stack( - alignment: Alignment.center, - children: [ - DismissiblePage( - backgroundColor: Colors.black, - onDismissed: () { - Navigator.pop(context); - }, - onDragUpdate: (details) { - logic.updateOpacity(details.opacity); - }, - direction: DismissiblePageDismissDirection.vertical, - isFullScreen: true, - minScale: 0.2, - dragSensitivity: 0.8, - startingOpacity: 0.9, - maxTransformValue: 0.6, - child: imageView, + return Stack( + alignment: Alignment.center, + children: [ + DismissiblePage( + backgroundColor: Colors.black, + onDismissed: () { + Navigator.pop(context); + }, + onDragUpdate: (details) { + logic.updateOpacity(details.opacity); + }, + direction: DismissiblePageDismissDirection.vertical, + minScale: 0.2, + dragSensitivity: 0.8, + startingOpacity: 0.9, + maxTransformValue: 0.6, + child: imageView, + ), + Positioned( + left: 24, + right: 24, + child: _buildPageButton( + previous: logic.previous, + next: logic.next, + opacity: state.opacity, + imageIndex: state.imageIndex, + imagePathList: state.imagePathList, ), - Positioned( - left: 24, - right: 24, - child: _buildPageButton( - previous: logic.previous, - next: logic.next, - opacity: state.opacity, + ), + Positioned( + bottom: 24, + left: 24, + right: 24, + child: SafeArea( + top: false, + child: _buildOperationButton( + onSaved: () { + MediaUtil.saveToGallery( + path: state.imagePathList[state.imageIndex.value], + type: MediaType.image, + ); + }, + onExit: () { + Navigator.pop(context); + }, imageIndex: state.imageIndex, imagePathList: state.imagePathList, + textStyle: textStyle.labelLarge, + opacity: state.opacity, ), ), - Positioned( - bottom: 24, - left: 24, - right: 24, - child: SafeArea( - child: _buildOperationButton( - onSaved: () { - MediaUtil.saveToGallery( - path: state.imagePathList[state.imageIndex.value], - type: MediaType.image, - ); - }, - onExit: () { - Navigator.pop(context); - }, - imageIndex: state.imageIndex, - imagePathList: state.imagePathList, - textStyle: textStyle.labelLarge, - opacity: state.opacity, - ), - ), - ), - ], - ), + ), + ], ); }, ); @@ -252,6 +247,10 @@ class _ImageViewGalleryState extends State { imageProvider: FileImage(File(widget.imagePathList[0])), backgroundDecoration: const BoxDecoration(color: Colors.transparent), initialScale: PhotoViewComputedScale.contained, + minScale: PhotoViewComputedScale.contained, + onTapDown: (context, details, controllerValue) { + Navigator.pop(context); + }, heroAttributes: PhotoViewHeroAttributes( tag: '${widget._heroTagPrefix}0', ), @@ -266,10 +265,14 @@ class _ImageViewGalleryState extends State { heroAttributes: PhotoViewHeroAttributes( tag: '${widget._heroTagPrefix}$index', ), + onTapDown: (context, details, controllerValue) { + Navigator.pop(context); + }, backgroundDecoration: const BoxDecoration( color: Colors.transparent, ), initialScale: PhotoViewComputedScale.contained, + minScale: PhotoViewComputedScale.contained, ), ); }, diff --git a/lib/router/app_pages.dart b/lib/router/app_pages.dart index ab67e44..da01930 100644 --- a/lib/router/app_pages.dart +++ b/lib/router/app_pages.dart @@ -224,6 +224,15 @@ class _MoodiaryPageTransition implements CustomTransition { ) { final pageRoute = ModalRoute.of(context) as PageRoute; if (Platform.isAndroid) { + if (pageRoute.popGestureInProgress) { + return const PredictiveBackPageTransitionsBuilder().buildTransitions( + pageRoute, + context, + animation, + secondaryAnimation, + child, + ); + } return const FadeForwardsPageTransitionsBuilder().buildTransitions( pageRoute, context,