mod: 图片预览UX
This commit is contained in:
@ -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),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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()),
|
||||||
// 设置
|
// 设置
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user