mirror of
https://github.com/HChaZZY/PiliPlus.git
synced 2025-12-06 09:13:48 +08:00
feat: use interactiveviewer gallery
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// https://github.com/qq326646683/interactiveviewer_gallery
|
||||
|
||||
/// A [PageRoute] with a semi transparent background.
|
||||
///
|
||||
/// Similar to calling [showDialog] except it can be used with a [Navigator] to
|
||||
/// show a [Hero] animation.
|
||||
class HeroDialogRoute<T> extends PageRoute<T> {
|
||||
HeroDialogRoute({
|
||||
required this.builder,
|
||||
});
|
||||
|
||||
final WidgetBuilder builder;
|
||||
|
||||
@override
|
||||
bool get opaque => false;
|
||||
|
||||
@override
|
||||
bool get barrierDismissible => true;
|
||||
|
||||
@override
|
||||
String? get barrierLabel => null;
|
||||
|
||||
@override
|
||||
Duration get transitionDuration => const Duration(milliseconds: 300);
|
||||
|
||||
@override
|
||||
bool get maintainState => true;
|
||||
|
||||
@override
|
||||
Color? get barrierColor => null;
|
||||
|
||||
@override
|
||||
Widget buildTransitions(
|
||||
BuildContext context,
|
||||
Animation<double> animation,
|
||||
Animation<double> secondaryAnimation,
|
||||
Widget child,
|
||||
) {
|
||||
return FadeTransition(
|
||||
opacity: CurvedAnimation(parent: animation, curve: Curves.easeOut),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildPage(
|
||||
BuildContext context,
|
||||
Animation<double> animation,
|
||||
Animation<double> secondaryAnimation,
|
||||
) {
|
||||
final Widget child = builder(context);
|
||||
final Widget result = Semantics(
|
||||
scopesRoute: true,
|
||||
explicitChildNodes: true,
|
||||
child: child,
|
||||
);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
1503
lib/common/widgets/interactiveviewer_gallery/interactive_viewer.dart
Normal file
1503
lib/common/widgets/interactiveviewer_gallery/interactive_viewer.dart
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,236 @@
|
||||
import 'interactive_viewer.dart' as custom;
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// https://github.com/qq326646683/interactiveviewer_gallery
|
||||
|
||||
/// A callback for the [InteractiveViewerBoundary] that is called when the scale
|
||||
/// changed.
|
||||
typedef ScaleChanged = void Function(double scale);
|
||||
|
||||
/// Builds an [InteractiveViewer] and provides callbacks that are called when a
|
||||
/// horizontal boundary has been hit.
|
||||
///
|
||||
/// The callbacks are called when an interaction ends by listening to the
|
||||
/// [InteractiveViewer.onInteractionEnd] callback.
|
||||
class InteractiveViewerBoundary extends StatefulWidget {
|
||||
const InteractiveViewerBoundary({
|
||||
super.key,
|
||||
required this.child,
|
||||
required this.boundaryWidth,
|
||||
this.controller,
|
||||
this.onScaleChanged,
|
||||
this.onLeftBoundaryHit,
|
||||
this.onRightBoundaryHit,
|
||||
this.onNoBoundaryHit,
|
||||
required this.maxScale,
|
||||
required this.minScale,
|
||||
this.onDismissed,
|
||||
this.dismissThreshold = 0.2,
|
||||
});
|
||||
|
||||
final double dismissThreshold;
|
||||
final VoidCallback? onDismissed;
|
||||
|
||||
final Widget child;
|
||||
|
||||
/// The max width this widget can have.
|
||||
///
|
||||
/// If the [InteractiveViewer] can take up the entire screen width, this
|
||||
/// should be set to `MediaQuery.of(context).size.width`.
|
||||
final double boundaryWidth;
|
||||
|
||||
/// The [TransformationController] for the [InteractiveViewer].
|
||||
final custom.TransformationController? controller;
|
||||
|
||||
/// Called when the scale changed after an interaction ended.
|
||||
final ScaleChanged? onScaleChanged;
|
||||
|
||||
/// Called when the left boundary has been hit after an interaction ended.
|
||||
final VoidCallback? onLeftBoundaryHit;
|
||||
|
||||
/// Called when the right boundary has been hit after an interaction ended.
|
||||
final VoidCallback? onRightBoundaryHit;
|
||||
|
||||
/// Called when no boundary has been hit after an interaction ended.
|
||||
final VoidCallback? onNoBoundaryHit;
|
||||
|
||||
final double maxScale;
|
||||
|
||||
final double minScale;
|
||||
|
||||
@override
|
||||
InteractiveViewerBoundaryState createState() =>
|
||||
InteractiveViewerBoundaryState();
|
||||
}
|
||||
|
||||
class InteractiveViewerBoundaryState extends State<InteractiveViewerBoundary>
|
||||
with SingleTickerProviderStateMixin {
|
||||
custom.TransformationController? _controller;
|
||||
|
||||
double? _scale;
|
||||
|
||||
late AnimationController _animateController;
|
||||
late Animation<Offset> _slideAnimation;
|
||||
late Animation<double> _scaleAnimation;
|
||||
late Animation<Decoration> _opacityAnimation;
|
||||
|
||||
Offset _offset = Offset.zero;
|
||||
bool _dragging = false;
|
||||
|
||||
bool get _isActive => _dragging || _animateController.isAnimating;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_controller = widget.controller ?? custom.TransformationController();
|
||||
|
||||
_animateController = AnimationController(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_updateMoveAnimation();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller!.dispose();
|
||||
_animateController.dispose();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _updateMoveAnimation() {
|
||||
final double endX = _offset.dx.sign * (_offset.dx.abs() / _offset.dy.abs());
|
||||
final double endY = _offset.dy.sign;
|
||||
|
||||
_slideAnimation = _animateController.drive(
|
||||
Tween<Offset>(
|
||||
begin: Offset.zero,
|
||||
end: Offset(endX, endY),
|
||||
),
|
||||
);
|
||||
|
||||
_scaleAnimation = _animateController.drive(
|
||||
Tween<double>(
|
||||
begin: 1,
|
||||
end: 0.25,
|
||||
),
|
||||
);
|
||||
|
||||
_opacityAnimation = _animateController.drive(
|
||||
DecorationTween(
|
||||
begin: const BoxDecoration(
|
||||
color: Colors.black,
|
||||
),
|
||||
end: const BoxDecoration(
|
||||
color: Colors.transparent,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleDragStart(ScaleStartDetails details) {
|
||||
_dragging = true;
|
||||
|
||||
if (_animateController.isAnimating) {
|
||||
_animateController.stop();
|
||||
} else {
|
||||
_offset = Offset.zero;
|
||||
_animateController.value = 0.0;
|
||||
}
|
||||
setState(_updateMoveAnimation);
|
||||
}
|
||||
|
||||
void _handleDragUpdate(ScaleUpdateDetails details) {
|
||||
if (!_isActive || _animateController.isAnimating) {
|
||||
return;
|
||||
}
|
||||
|
||||
_offset += details.focalPointDelta;
|
||||
|
||||
setState(_updateMoveAnimation);
|
||||
|
||||
if (!_animateController.isAnimating) {
|
||||
_animateController.value = _offset.dy.abs() / context.size!.height;
|
||||
}
|
||||
}
|
||||
|
||||
void _handleDragEnd(ScaleEndDetails details) {
|
||||
if (!_isActive || _animateController.isAnimating) {
|
||||
return;
|
||||
}
|
||||
|
||||
_dragging = false;
|
||||
|
||||
if (_animateController.isCompleted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_animateController.isDismissed) {
|
||||
// if the dragged value exceeded the dismissThreshold, call onDismissed
|
||||
// else animate back to initial position.
|
||||
if (_animateController.value > widget.dismissThreshold) {
|
||||
widget.onDismissed?.call();
|
||||
} else {
|
||||
_animateController.reverse();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _updateBoundaryDetection() {
|
||||
final double scale = _controller!.value.row0[0];
|
||||
|
||||
if (_scale != scale) {
|
||||
// the scale changed
|
||||
_scale = scale;
|
||||
widget.onScaleChanged?.call(scale);
|
||||
}
|
||||
|
||||
if (scale <= 1.01) {
|
||||
// cant hit any boundaries when the child is not scaled
|
||||
return;
|
||||
}
|
||||
|
||||
final double xOffset = _controller!.value.row0[3];
|
||||
final double boundaryWidth = widget.boundaryWidth;
|
||||
final double boundaryEnd = boundaryWidth * scale;
|
||||
final double xPos = boundaryEnd + xOffset;
|
||||
|
||||
if (boundaryEnd.round() == xPos.round()) {
|
||||
// left boundary hit
|
||||
widget.onLeftBoundaryHit?.call();
|
||||
} else if (boundaryWidth.round() == xPos.round()) {
|
||||
// right boundary hit
|
||||
widget.onRightBoundaryHit?.call();
|
||||
} else {
|
||||
widget.onNoBoundaryHit?.call();
|
||||
}
|
||||
}
|
||||
|
||||
Widget get content => DecoratedBoxTransition(
|
||||
decoration: _opacityAnimation,
|
||||
child: SlideTransition(
|
||||
position: _slideAnimation,
|
||||
child: ScaleTransition(
|
||||
scale: _scaleAnimation,
|
||||
child: widget.child,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return custom.InteractiveViewer(
|
||||
maxScale: widget.maxScale,
|
||||
minScale: widget.minScale,
|
||||
transformationController: _controller,
|
||||
onInteractionEnd: (_) => _updateBoundaryDetection(),
|
||||
onPanStart: _handleDragStart,
|
||||
onPanUpdate: _handleDragUpdate,
|
||||
onPanEnd: _handleDragEnd,
|
||||
child: content,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,491 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:PiliPalaX/utils/download.dart';
|
||||
import 'package:PiliPalaX/utils/storage.dart';
|
||||
import 'package:PiliPalaX/utils/utils.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:status_bar_control/status_bar_control.dart';
|
||||
import 'interactive_viewer_boundary.dart';
|
||||
import 'interactive_viewer.dart' as custom;
|
||||
|
||||
/// https://github.com/qq326646683/interactiveviewer_gallery
|
||||
|
||||
/// Builds a carousel controlled by a [PageView] for the tweet media sources.
|
||||
///
|
||||
/// Used for showing a full screen view of the [TweetMedia] sources.
|
||||
///
|
||||
/// The sources can be panned and zoomed interactively using an
|
||||
/// [InteractiveViewer].
|
||||
/// An [InteractiveViewerBoundary] is used to detect when the boundary of the
|
||||
/// source is hit after zooming in to disable or enable the swiping gesture of
|
||||
/// the [PageView].
|
||||
///
|
||||
typedef IndexedFocusedWidgetBuilder = Widget Function(
|
||||
BuildContext context, int index, bool isFocus, bool enablePageView);
|
||||
|
||||
typedef IndexedTagStringBuilder = String Function(int index);
|
||||
|
||||
class InteractiveviewerGallery<T> extends StatefulWidget {
|
||||
const InteractiveviewerGallery({
|
||||
super.key,
|
||||
required this.sources,
|
||||
required this.initIndex,
|
||||
this.itemBuilder,
|
||||
this.maxScale = 8,
|
||||
this.minScale = 1.0,
|
||||
this.onPageChanged,
|
||||
this.onDismissed,
|
||||
});
|
||||
|
||||
/// The sources to show.
|
||||
final List<T> sources;
|
||||
|
||||
/// The index of the first source in [sources] to show.
|
||||
final int initIndex;
|
||||
|
||||
/// The item content
|
||||
final IndexedFocusedWidgetBuilder? itemBuilder;
|
||||
|
||||
final double maxScale;
|
||||
|
||||
final double minScale;
|
||||
|
||||
final ValueChanged<int>? onPageChanged;
|
||||
|
||||
final ValueChanged<int>? onDismissed;
|
||||
|
||||
@override
|
||||
State<InteractiveviewerGallery> createState() =>
|
||||
_InteractiveviewerGalleryState();
|
||||
}
|
||||
|
||||
class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
with SingleTickerProviderStateMixin {
|
||||
PageController? _pageController;
|
||||
custom.TransformationController? _transformationController;
|
||||
|
||||
/// The controller to animate the transformation value of the
|
||||
/// [InteractiveViewer] when it should reset.
|
||||
late AnimationController _animationController;
|
||||
Animation<Matrix4>? _animation;
|
||||
|
||||
/// `true` when an source is zoomed in and not at the at a horizontal boundary
|
||||
/// to disable the [PageView].
|
||||
bool _enablePageView = true;
|
||||
|
||||
late Offset _doubleTapLocalPosition;
|
||||
|
||||
int? currentIndex;
|
||||
|
||||
late List<bool> _thumbList;
|
||||
late int _quality;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_quality =
|
||||
GStorage.setting.get(SettingBoxKey.previewQuality, defaultValue: 80);
|
||||
_thumbList = List.generate(widget.sources.length, (_) => true);
|
||||
|
||||
_pageController = PageController(initialPage: widget.initIndex);
|
||||
|
||||
_transformationController = custom.TransformationController();
|
||||
|
||||
_animationController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
)..addListener(() {
|
||||
_transformationController!.value =
|
||||
_animation?.value ?? Matrix4.identity();
|
||||
});
|
||||
|
||||
currentIndex = widget.initIndex;
|
||||
setStatusBar();
|
||||
}
|
||||
|
||||
setStatusBar() async {
|
||||
if (Platform.isIOS || Platform.isAndroid) {
|
||||
await StatusBarControl.setHidden(
|
||||
true,
|
||||
animation: StatusBarAnimation.FADE,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() async {
|
||||
_pageController?.dispose();
|
||||
_animationController.removeListener(() {});
|
||||
_animationController.dispose();
|
||||
if (Platform.isIOS || Platform.isAndroid) {
|
||||
StatusBarControl.setHidden(false, animation: StatusBarAnimation.FADE);
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// When the source gets scaled up, the swipe up / down to dismiss gets
|
||||
/// disabled.
|
||||
///
|
||||
/// When the scale resets, the dismiss and the page view swiping gets enabled.
|
||||
void _onScaleChanged(double scale) {
|
||||
final bool initialScale = scale <= widget.minScale;
|
||||
|
||||
if (initialScale) {
|
||||
if (!_enablePageView) {
|
||||
setState(() {
|
||||
_enablePageView = true;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (_enablePageView) {
|
||||
setState(() {
|
||||
_enablePageView = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// When the left boundary has been hit after scaling up the source, the page
|
||||
/// view swiping gets enabled if it has a page to swipe to.
|
||||
void _onLeftBoundaryHit() {
|
||||
if (!_enablePageView && _pageController!.page!.floor() > 0) {
|
||||
setState(() {
|
||||
_enablePageView = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// When the right boundary has been hit after scaling up the source, the page
|
||||
/// view swiping gets enabled if it has a page to swipe to.
|
||||
void _onRightBoundaryHit() {
|
||||
if (!_enablePageView &&
|
||||
_pageController!.page!.floor() < widget.sources.length - 1) {
|
||||
setState(() {
|
||||
_enablePageView = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// When the source has been scaled up and no horizontal boundary has been hit,
|
||||
/// the page view swiping gets disabled.
|
||||
void _onNoBoundaryHit() {
|
||||
if (_enablePageView) {
|
||||
setState(() {
|
||||
_enablePageView = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// When the page view changed its page, the source will animate back into the
|
||||
/// original scale if it was scaled up.
|
||||
///
|
||||
/// Additionally the swipe up / down to dismiss gets enabled.
|
||||
void _onPageChanged(int page) {
|
||||
setState(() {
|
||||
currentIndex = page;
|
||||
});
|
||||
widget.onPageChanged?.call(page);
|
||||
if (_transformationController!.value != Matrix4.identity()) {
|
||||
// animate the reset for the transformation of the interactive viewer
|
||||
|
||||
_animation = Matrix4Tween(
|
||||
begin: _transformationController!.value,
|
||||
end: Matrix4.identity(),
|
||||
).animate(
|
||||
CurveTween(curve: Curves.easeOut).animate(_animationController),
|
||||
);
|
||||
|
||||
_animationController.forward(from: 0);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
InteractiveViewerBoundary(
|
||||
controller: _transformationController,
|
||||
boundaryWidth: MediaQuery.of(context).size.width,
|
||||
onScaleChanged: _onScaleChanged,
|
||||
onLeftBoundaryHit: _onLeftBoundaryHit,
|
||||
onRightBoundaryHit: _onRightBoundaryHit,
|
||||
onNoBoundaryHit: _onNoBoundaryHit,
|
||||
maxScale: widget.maxScale,
|
||||
minScale: widget.minScale,
|
||||
onDismissed: () {
|
||||
Get.back();
|
||||
widget.onDismissed?.call(_pageController!.page!.floor());
|
||||
},
|
||||
child: PageView.builder(
|
||||
onPageChanged: _onPageChanged,
|
||||
controller: _pageController,
|
||||
physics:
|
||||
_enablePageView ? null : const NeverScrollableScrollPhysics(),
|
||||
itemCount: widget.sources.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return GestureDetector(
|
||||
onDoubleTapDown: (TapDownDetails details) {
|
||||
_doubleTapLocalPosition = details.localPosition;
|
||||
},
|
||||
onDoubleTap: onDoubleTap,
|
||||
onLongPress: onLongPress,
|
||||
child: widget.itemBuilder != null
|
||||
? widget.itemBuilder!(
|
||||
context,
|
||||
index,
|
||||
index == currentIndex,
|
||||
_enablePageView,
|
||||
)
|
||||
: _itemBuilder(widget.sources, index),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
12,
|
||||
8,
|
||||
20,
|
||||
MediaQuery.of(context).padding.bottom + 8,
|
||||
),
|
||||
decoration: _enablePageView
|
||||
? BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Colors.transparent,
|
||||
Colors.black.withOpacity(0.3)
|
||||
],
|
||||
),
|
||||
)
|
||||
: null,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, color: Colors.white),
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
widget.onDismissed?.call(_pageController!.page!.floor());
|
||||
},
|
||||
),
|
||||
widget.sources.length > 1
|
||||
? Text(
|
||||
"${currentIndex! + 1}/${widget.sources.length}",
|
||||
style: const TextStyle(color: Colors.white),
|
||||
)
|
||||
: const SizedBox(),
|
||||
PopupMenuButton(
|
||||
itemBuilder: (context) {
|
||||
return [
|
||||
PopupMenuItem(
|
||||
value: 0,
|
||||
onTap: () => onShareImg(widget.sources[currentIndex!]),
|
||||
child: const Text("分享图片"),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 1,
|
||||
onTap: () {
|
||||
Utils.copyText(widget.sources[currentIndex!]);
|
||||
},
|
||||
child: const Text("复制链接"),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 2,
|
||||
onTap: () {
|
||||
DownloadUtils.downloadImg(
|
||||
context,
|
||||
[widget.sources[currentIndex!]],
|
||||
);
|
||||
},
|
||||
child: const Text("保存图片"),
|
||||
),
|
||||
if (widget.sources.length > 1)
|
||||
PopupMenuItem(
|
||||
value: 3,
|
||||
onTap: () {
|
||||
DownloadUtils.downloadImg(
|
||||
context,
|
||||
widget.sources[currentIndex!],
|
||||
);
|
||||
},
|
||||
child: const Text("保存全部图片"),
|
||||
),
|
||||
];
|
||||
},
|
||||
child: const Icon(Icons.more_horiz, color: Colors.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// 图片分享
|
||||
void onShareImg(String imgUrl) async {
|
||||
SmartDialog.showLoading();
|
||||
var response = await Dio()
|
||||
.get(imgUrl, options: Options(responseType: ResponseType.bytes));
|
||||
final temp = await getTemporaryDirectory();
|
||||
SmartDialog.dismiss();
|
||||
String imgName =
|
||||
"plpl_pic_${DateTime.now().toString().split('-').join()}.jpg";
|
||||
var path = '${temp.path}/$imgName';
|
||||
File(path).writeAsBytesSync(response.data);
|
||||
Share.shareXFiles([XFile(path)], subject: imgUrl);
|
||||
}
|
||||
|
||||
Widget _itemBuilder(sources, index) {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
Get.back();
|
||||
},
|
||||
child: Center(
|
||||
child: Hero(
|
||||
tag: sources[index],
|
||||
child: CachedNetworkImage(
|
||||
fadeInDuration: const Duration(milliseconds: 0),
|
||||
fadeOutDuration: const Duration(milliseconds: 0),
|
||||
imageUrl: _thumbList[index] && _quality != 100
|
||||
? '${sources[index]}@${_quality}q.webp'
|
||||
: sources[index],
|
||||
fit: BoxFit.contain,
|
||||
progressIndicatorBuilder: (context, url, progress) {
|
||||
return Container(
|
||||
width: 150.0,
|
||||
alignment: Alignment.center,
|
||||
child: LinearProgressIndicator(value: progress.progress ?? 0),
|
||||
);
|
||||
},
|
||||
errorListener: (value) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
setState(() {
|
||||
_thumbList[index] = false;
|
||||
});
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
onDoubleTap() {
|
||||
Matrix4 matrix = _transformationController!.value.clone();
|
||||
double currentScale = matrix.row0.x;
|
||||
|
||||
double targetScale = widget.minScale;
|
||||
|
||||
if (currentScale <= widget.minScale) {
|
||||
targetScale = widget.maxScale * 0.7;
|
||||
}
|
||||
|
||||
double offSetX = targetScale == 1.0
|
||||
? 0.0
|
||||
: -_doubleTapLocalPosition.dx * (targetScale - 1);
|
||||
double offSetY = targetScale == 1.0
|
||||
? 0.0
|
||||
: -_doubleTapLocalPosition.dy * (targetScale - 1);
|
||||
|
||||
matrix = Matrix4.fromList([
|
||||
targetScale,
|
||||
matrix.row1.x,
|
||||
matrix.row2.x,
|
||||
matrix.row3.x,
|
||||
matrix.row0.y,
|
||||
targetScale,
|
||||
matrix.row2.y,
|
||||
matrix.row3.y,
|
||||
matrix.row0.z,
|
||||
matrix.row1.z,
|
||||
targetScale,
|
||||
matrix.row3.z,
|
||||
offSetX,
|
||||
offSetY,
|
||||
matrix.row2.w,
|
||||
matrix.row3.w
|
||||
]);
|
||||
|
||||
_animation = Matrix4Tween(
|
||||
begin: _transformationController!.value,
|
||||
end: matrix,
|
||||
).animate(
|
||||
CurveTween(curve: Curves.easeOut).animate(_animationController),
|
||||
);
|
||||
_animationController
|
||||
.forward(from: 0)
|
||||
.whenComplete(() => _onScaleChanged(targetScale));
|
||||
}
|
||||
|
||||
onLongPress() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 12),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
onTap: () {
|
||||
onShareImg(widget.sources[currentIndex!]);
|
||||
Get.back();
|
||||
},
|
||||
dense: true,
|
||||
title: const Text('分享', style: TextStyle(fontSize: 14)),
|
||||
),
|
||||
ListTile(
|
||||
onTap: () {
|
||||
Get.back();
|
||||
Utils.copyText(widget.sources[currentIndex!]);
|
||||
},
|
||||
dense: true,
|
||||
title: const Text('复制链接', style: TextStyle(fontSize: 14)),
|
||||
),
|
||||
ListTile(
|
||||
onTap: () {
|
||||
Get.back();
|
||||
DownloadUtils.downloadImg(
|
||||
context,
|
||||
[widget.sources[currentIndex!]],
|
||||
);
|
||||
},
|
||||
dense: true,
|
||||
title: const Text('保存图片', style: TextStyle(fontSize: 14)),
|
||||
),
|
||||
if (widget.sources.length > 1)
|
||||
ListTile(
|
||||
onTap: () {
|
||||
Get.back();
|
||||
DownloadUtils.downloadImg(
|
||||
context,
|
||||
widget.sources as List<String>,
|
||||
);
|
||||
},
|
||||
dense: true,
|
||||
title: const Text('保存全部图片', style: TextStyle(fontSize: 14)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user