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,45 +43,22 @@ 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, // Note that scrollable widget inside DismissiblePage might limit the functionality
centerTitle: false, // If scroll direction matches DismissiblePage direction
title: Obx( direction: DismissiblePageDismissDirection.down,
() => Text.rich( disabled: _dismissDisabled,
TextSpan(children: [ isFullScreen: true,
TextSpan(text: _previewController.currentPage.toString()), child: Hero(
const TextSpan(text: ' / '), tag: _previewController
TextSpan(text: _previewController.imgList.length.toString()), .imgList[_previewController.initialPage.value],
]), child: GestureDetector(
),
),
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: () { onTap: () {
_previewController.visiable = !_previewController.visiable; _previewController.visiable = !_previewController.visiable;
setState(() {}); setState(() {});
@ -103,7 +82,8 @@ class _ImagePreviewState extends State<ImagePreview>
fit: BoxFit.contain, fit: BoxFit.contain,
mode: ExtendedImageMode.gesture, mode: ExtendedImageMode.gesture,
onDoubleTap: (ExtendedImageGestureState state) { onDoubleTap: (ExtendedImageGestureState state) {
final Offset? pointerDownPosition = state.pointerDownPosition; final Offset? pointerDownPosition =
state.pointerDownPosition;
final double? begin = state.gestureDetails!.totalScale; final double? begin = state.gestureDetails!.totalScale;
double end; double end;
@ -118,8 +98,14 @@ class _ImagePreviewState extends State<ImagePreview>
_doubleClickAnimationController.reset(); _doubleClickAnimationController.reset();
if (begin == doubleTapScales[0]) { if (begin == doubleTapScales[0]) {
setState(() {
_dismissDisabled = true;
});
end = doubleTapScales[1]; end = doubleTapScales[1];
} else { } else {
setState(() {
_dismissDisabled = false;
});
end = doubleTapScales[0]; end = doubleTapScales[0];
} }
@ -174,10 +160,55 @@ class _ImagePreviewState extends State<ImagePreview>
}, },
), ),
), ),
floatingActionButton: FloatingActionButton( ),
),
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(), onPressed: () => _previewController.onSaveImg(),
child: const Icon(Icons.save_alt_rounded), 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: