feat: improve image handling with enhanced hero transitions and dynamic sizing

This commit is contained in:
ZhuJHua
2025-02-23 02:17:17 +08:00
parent b9d694ca50
commit e5578bdeae
7 changed files with 175 additions and 120 deletions

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,
),
),
),
],
);

View File

@@ -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<String> videoList;
const MediaVideoComponent(
{super.key, required this.dateTime, required this.videoList});
const MediaVideoComponent({
super.key,
required this.dateTime,
required this.videoList,
});
//点击视频跳转到视频预览
void _toVideoView(List<String> 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),
],
);
},

View File

@@ -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)),
),
),
),
),

View File

@@ -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<ImageViewGallery> {
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<ImageViewGallery> {
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,
),
);
},

View File

@@ -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,