mod: 图片预览UX

This commit is contained in:
guozhigq
2023-05-26 21:40:16 +08:00
parent abf0272314
commit 0a4f6b2508
4 changed files with 184 additions and 138 deletions

View File

@ -1,3 +1,4 @@
import 'package:dismissible_page/dismissible_page.dart';
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import 'package:extended_image/extended_image.dart';
@ -16,24 +17,25 @@ class ImagePreview extends StatefulWidget {
class _ImagePreviewState extends State<ImagePreview>
with TickerProviderStateMixin {
final PreviewController _previewController = Get.put(PreviewController());
late AnimationController animationController;
// late AnimationController animationController;
late AnimationController _doubleClickAnimationController;
Animation<double>? _doubleClickAnimation;
late DoubleClickAnimationListener _doubleClickAnimationListener;
List<double> doubleTapScales = <double>[1.0, 2.0];
bool _dismissDisabled = false;
@override
void initState() {
super.initState();
animationController = AnimationController(
vsync: this, duration: const Duration(milliseconds: 400));
// animationController = AnimationController(
// vsync: this, duration: const Duration(milliseconds: 400));
_doubleClickAnimationController = AnimationController(
duration: const Duration(milliseconds: 250), vsync: this);
}
@override
void dispose() {
animationController.dispose();
// animationController.dispose();
_doubleClickAnimationController.dispose();
clearGestureDetailsCache();
super.dispose();
@ -41,143 +43,172 @@ class _ImagePreviewState extends State<ImagePreview>
@override
Widget build(BuildContext context) {
return Scaffold(
extendBodyBehindAppBar: true,
appBar: AppBarWidget(
controller: animationController,
visible: _previewController.visiable,
child: AppBar(
backgroundColor: Theme.of(context).colorScheme.background,
elevation: 0,
centerTitle: false,
title: Obx(
() => Text.rich(
TextSpan(children: [
TextSpan(text: _previewController.currentPage.toString()),
const TextSpan(text: ' / '),
TextSpan(text: _previewController.imgList.length.toString()),
]),
),
),
actions: [
PopupMenuButton(
icon: const Icon(Icons.more_vert),
tooltip: 'action',
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
PopupMenuItem(
value: 'share',
onTap: _previewController.onShareImg,
child: const Text('分享'),
),
PopupMenuItem(
value: 'save',
onTap: _previewController.onSaveImg,
child: const Text('保存'),
),
],
),
],
),
),
body: GestureDetector(
onTap: () {
_previewController.visiable = !_previewController.visiable;
setState(() {});
},
child: ExtendedImageGesturePageView.builder(
controller: ExtendedPageController(
initialPage: _previewController.initialPage.value,
pageSpacing: 0,
),
onPageChanged: (int index) {
_previewController.initialPage.value = index;
_previewController.currentPage.value = index + 1;
return Stack(
children: [
DismissiblePage(
backgroundColor: Colors.transparent,
onDismissed: () {
Navigator.of(context).pop();
},
canScrollPage: (GestureDetails? gestureDetails) =>
gestureDetails!.totalScale! <= 1.0,
preloadPagesCount: 2,
itemCount: _previewController.imgList.length,
itemBuilder: (BuildContext context, int index) {
return ExtendedImage.network(
_previewController.imgList[index],
fit: BoxFit.contain,
mode: ExtendedImageMode.gesture,
onDoubleTap: (ExtendedImageGestureState state) {
final Offset? pointerDownPosition = state.pointerDownPosition;
final double? begin = state.gestureDetails!.totalScale;
double end;
//remove old
_doubleClickAnimation
?.removeListener(_doubleClickAnimationListener);
//stop pre
_doubleClickAnimationController.stop();
//reset to use
_doubleClickAnimationController.reset();
if (begin == doubleTapScales[0]) {
end = doubleTapScales[1];
} else {
end = doubleTapScales[0];
}
_doubleClickAnimationListener = () {
state.handleDoubleTap(
scale: _doubleClickAnimation!.value,
doubleTapPosition: pointerDownPosition);
};
_doubleClickAnimation = _doubleClickAnimationController
.drive(Tween<double>(begin: begin, end: end));
_doubleClickAnimation!
.addListener(_doubleClickAnimationListener);
_doubleClickAnimationController.forward();
// Note that scrollable widget inside DismissiblePage might limit the functionality
// If scroll direction matches DismissiblePage direction
direction: DismissiblePageDismissDirection.down,
disabled: _dismissDisabled,
isFullScreen: true,
child: Hero(
tag: _previewController
.imgList[_previewController.initialPage.value],
child: GestureDetector(
onTap: () {
_previewController.visiable = !_previewController.visiable;
setState(() {});
},
loadStateChanged: (ExtendedImageState state) {
if (state.extendedImageLoadState == LoadState.loading) {
final ImageChunkEvent? loadingProgress =
state.loadingProgress;
final double? progress =
loadingProgress?.expectedTotalBytes != null
? loadingProgress!.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null;
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
SizedBox(
width: 150.0,
child: LinearProgressIndicator(value: progress),
),
const SizedBox(height: 10.0),
Text('${((progress ?? 0.0) * 100).toInt()}%'),
],
),
child: ExtendedImageGesturePageView.builder(
controller: ExtendedPageController(
initialPage: _previewController.initialPage.value,
pageSpacing: 0,
),
onPageChanged: (int index) {
_previewController.initialPage.value = index;
_previewController.currentPage.value = index + 1;
},
canScrollPage: (GestureDetails? gestureDetails) =>
gestureDetails!.totalScale! <= 1.0,
preloadPagesCount: 2,
itemCount: _previewController.imgList.length,
itemBuilder: (BuildContext context, int index) {
return ExtendedImage.network(
_previewController.imgList[index],
fit: BoxFit.contain,
mode: ExtendedImageMode.gesture,
onDoubleTap: (ExtendedImageGestureState state) {
final Offset? pointerDownPosition =
state.pointerDownPosition;
final double? begin = state.gestureDetails!.totalScale;
double end;
//remove old
_doubleClickAnimation
?.removeListener(_doubleClickAnimationListener);
//stop pre
_doubleClickAnimationController.stop();
//reset to use
_doubleClickAnimationController.reset();
if (begin == doubleTapScales[0]) {
setState(() {
_dismissDisabled = true;
});
end = doubleTapScales[1];
} else {
setState(() {
_dismissDisabled = false;
});
end = doubleTapScales[0];
}
_doubleClickAnimationListener = () {
state.handleDoubleTap(
scale: _doubleClickAnimation!.value,
doubleTapPosition: pointerDownPosition);
};
_doubleClickAnimation = _doubleClickAnimationController
.drive(Tween<double>(begin: begin, end: end));
_doubleClickAnimation!
.addListener(_doubleClickAnimationListener);
_doubleClickAnimationController.forward();
},
loadStateChanged: (ExtendedImageState state) {
if (state.extendedImageLoadState == LoadState.loading) {
final ImageChunkEvent? loadingProgress =
state.loadingProgress;
final double? progress =
loadingProgress?.expectedTotalBytes != null
? loadingProgress!.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null;
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
SizedBox(
width: 150.0,
child: LinearProgressIndicator(value: progress),
),
const SizedBox(height: 10.0),
Text('${((progress ?? 0.0) * 100).toInt()}%'),
],
),
);
}
},
initGestureConfigHandler: (ExtendedImageState state) {
return GestureConfig(
inPageView: true,
initialScale: 1.0,
maxScale: 5.0,
animationMaxScale: 6.0,
initialAlignment: InitialAlignment.center,
);
},
);
}
},
initGestureConfigHandler: (ExtendedImageState state) {
return GestureConfig(
inPageView: true,
initialScale: 1.0,
maxScale: 5.0,
animationMaxScale: 6.0,
initialAlignment: InitialAlignment.center,
);
},
);
},
},
),
),
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => _previewController.onSaveImg(),
child: const Icon(Icons.save_alt_rounded),
),
Positioned(
left: 0,
right: 0,
bottom: 0,
child: Container(
// height: 45,
padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom + 10, top: 20),
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: <Color>[
Colors.transparent,
Colors.black87,
],
tileMode: TileMode.mirror,
),
),
child: Padding(
padding: const EdgeInsets.only(left: 20, right: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Obx(
() => Text.rich(
TextSpan(
style: const TextStyle(
color: Colors.white,
fontSize: 18
),
children: [
TextSpan(text: _previewController.currentPage.toString()),
const TextSpan(text: ' / '),
TextSpan(text: _previewController.imgList.length.toString()),
]),
),
),
FloatingActionButton(
onPressed: () => _previewController.onSaveImg(),
child: const Icon(Icons.save_alt_rounded),
),
],
),
),
),
),
],
);
}
}

View File

@ -19,7 +19,13 @@ class Routes {
// 视频详情
GetPage(name: '/video', page: () => const VideoDetailPage()),
// 图片预览
GetPage(name: '/preview', page: () => const ImagePreview()),
GetPage(
name: '/preview',
page: () => const ImagePreview(),
transition: Transition.fade,
transitionDuration: const Duration(milliseconds: 300),
showCupertinoParallax: false,
),
//
GetPage(name: '/webview', page: () => const WebviewPage()),
// 设置

View File

@ -281,6 +281,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.2.0"
dismissible_page:
dependency: "direct main"
description:
name: dismissible_page
sha256: "5b2316f770fe83583f770df1f6505cb19102081c5971979806e77f2e507a9958"
url: "https://pub.dev"
source: hosted
version: "1.0.2"
dynamic_color:
dependency: "direct main"
description:

View File

@ -72,6 +72,7 @@ dependencies:
font_awesome_flutter: ^10.4.0
# toast
flutter_smart_dialog: ^4.9.0+6
dismissible_page: ^1.0.2
dev_dependencies:
flutter_test: