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:get/get.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:extended_image/extended_image.dart'; import 'package:extended_image/extended_image.dart';
@ -16,24 +17,25 @@ class ImagePreview extends StatefulWidget {
class _ImagePreviewState extends State<ImagePreview> class _ImagePreviewState extends State<ImagePreview>
with TickerProviderStateMixin { with TickerProviderStateMixin {
final PreviewController _previewController = Get.put(PreviewController()); final PreviewController _previewController = Get.put(PreviewController());
late AnimationController animationController; // late AnimationController animationController;
late AnimationController _doubleClickAnimationController; late AnimationController _doubleClickAnimationController;
Animation<double>? _doubleClickAnimation; Animation<double>? _doubleClickAnimation;
late DoubleClickAnimationListener _doubleClickAnimationListener; late DoubleClickAnimationListener _doubleClickAnimationListener;
List<double> doubleTapScales = <double>[1.0, 2.0]; List<double> doubleTapScales = <double>[1.0, 2.0];
bool _dismissDisabled = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
animationController = AnimationController( // animationController = AnimationController(
vsync: this, duration: const Duration(milliseconds: 400)); // vsync: this, duration: const Duration(milliseconds: 400));
_doubleClickAnimationController = AnimationController( _doubleClickAnimationController = AnimationController(
duration: const Duration(milliseconds: 250), vsync: this); duration: const Duration(milliseconds: 250), vsync: this);
} }
@override @override
void dispose() { void dispose() {
animationController.dispose(); // animationController.dispose();
_doubleClickAnimationController.dispose(); _doubleClickAnimationController.dispose();
clearGestureDetailsCache(); clearGestureDetailsCache();
super.dispose(); super.dispose();
@ -41,143 +43,172 @@ class _ImagePreviewState extends State<ImagePreview>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Stack(
extendBodyBehindAppBar: true, children: [
appBar: AppBarWidget( DismissiblePage(
controller: animationController, backgroundColor: Colors.transparent,
visible: _previewController.visiable, onDismissed: () {
child: AppBar( Navigator.of(context).pop();
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;
}, },
canScrollPage: (GestureDetails? gestureDetails) => // Note that scrollable widget inside DismissiblePage might limit the functionality
gestureDetails!.totalScale! <= 1.0, // If scroll direction matches DismissiblePage direction
preloadPagesCount: 2, direction: DismissiblePageDismissDirection.down,
itemCount: _previewController.imgList.length, disabled: _dismissDisabled,
itemBuilder: (BuildContext context, int index) { isFullScreen: true,
return ExtendedImage.network( child: Hero(
_previewController.imgList[index], tag: _previewController
fit: BoxFit.contain, .imgList[_previewController.initialPage.value],
mode: ExtendedImageMode.gesture, child: GestureDetector(
onDoubleTap: (ExtendedImageGestureState state) { onTap: () {
final Offset? pointerDownPosition = state.pointerDownPosition; _previewController.visiable = !_previewController.visiable;
final double? begin = state.gestureDetails!.totalScale; setState(() {});
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();
}, },
loadStateChanged: (ExtendedImageState state) { child: ExtendedImageGesturePageView.builder(
if (state.extendedImageLoadState == LoadState.loading) { controller: ExtendedPageController(
final ImageChunkEvent? loadingProgress = initialPage: _previewController.initialPage.value,
state.loadingProgress; pageSpacing: 0,
final double? progress = ),
loadingProgress?.expectedTotalBytes != null onPageChanged: (int index) {
? loadingProgress!.cumulativeBytesLoaded / _previewController.initialPage.value = index;
loadingProgress.expectedTotalBytes! _previewController.currentPage.value = index + 1;
: null; },
return Center( canScrollPage: (GestureDetails? gestureDetails) =>
child: Column( gestureDetails!.totalScale! <= 1.0,
mainAxisAlignment: MainAxisAlignment.center, preloadPagesCount: 2,
crossAxisAlignment: CrossAxisAlignment.center, itemCount: _previewController.imgList.length,
children: <Widget>[ itemBuilder: (BuildContext context, int index) {
SizedBox( return ExtendedImage.network(
width: 150.0, _previewController.imgList[index],
child: LinearProgressIndicator(value: progress), fit: BoxFit.contain,
), mode: ExtendedImageMode.gesture,
const SizedBox(height: 10.0), onDoubleTap: (ExtendedImageGestureState state) {
Text('${((progress ?? 0.0) * 100).toInt()}%'), 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,
);
},
);
},
), ),
), Positioned(
floatingActionButton: FloatingActionButton( left: 0,
onPressed: () => _previewController.onSaveImg(), right: 0,
child: const Icon(Icons.save_alt_rounded), 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: '/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()), GetPage(name: '/webview', page: () => const WebviewPage()),
// 设置 // 设置

View File

@ -281,6 +281,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.0" 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: dynamic_color:
dependency: "direct main" dependency: "direct main"
description: description:

View File

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