476 lines
15 KiB
Dart
476 lines
15 KiB
Dart
import 'package:flutter/gestures.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:extended_image/extended_image.dart';
|
|
|
|
Map<Object?, GestureDetails?> _gestureDetailsCache =
|
|
<Object?, GestureDetails?>{};
|
|
|
|
///clear the gesture details
|
|
void clearGestureDetailsCache() {
|
|
_gestureDetailsCache.clear();
|
|
}
|
|
|
|
/// scale idea from https://github.com/flutter/flutter/blob/master/examples/layers/widgets/gestures.dart
|
|
/// zoom image
|
|
class GesturedImage extends ExtendedImageGesture {
|
|
const GesturedImage(ExtendedImageState extendedImageState,
|
|
{ImageBuilderForGesture? imageBuilder,
|
|
CanScaleImage? canScaleImage,
|
|
Key? key})
|
|
: super(extendedImageState,
|
|
imageBuilder: imageBuilder, canScaleImage: canScaleImage, key: key);
|
|
|
|
@override
|
|
GesturedImageState createState() => GesturedImageState();
|
|
}
|
|
|
|
class GesturedImageState extends ExtendedImageGestureState {
|
|
///details for gesture
|
|
GestureDetails? _gestureDetails;
|
|
late Offset _normalizedOffset;
|
|
double? _startingScale;
|
|
late Offset _startingOffset;
|
|
Offset? _pointerDownPosition;
|
|
late GestureAnimation _gestureAnimation;
|
|
GestureConfig? _gestureConfig;
|
|
ExtendedImageGesturePageViewState? _pageViewState;
|
|
@override
|
|
ExtendedImageSlidePageState? get extendedImageSlidePageState =>
|
|
widget.extendedImageState.slidePageState;
|
|
|
|
@override
|
|
GestureDetails? get gestureDetails => _gestureDetails;
|
|
|
|
@override
|
|
set gestureDetails(GestureDetails? value) {
|
|
if (mounted) {
|
|
setState(() {
|
|
_gestureDetails = value;
|
|
_gestureConfig?.gestureDetailsIsChanged?.call(_gestureDetails);
|
|
});
|
|
}
|
|
}
|
|
|
|
@override
|
|
GestureConfig? get imageGestureConfig => _gestureConfig;
|
|
|
|
@override
|
|
Offset? get pointerDownPosition => _pointerDownPosition;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (_gestureConfig!.cacheGesture) {
|
|
_gestureDetailsCache[widget.extendedImageState.imageStreamKey] =
|
|
_gestureDetails;
|
|
}
|
|
|
|
Widget image = ExtendedRawImage(
|
|
image: widget.extendedImageState.extendedImageInfo?.image,
|
|
width: widget.extendedImageState.imageWidget.width,
|
|
height: widget.extendedImageState.imageWidget.height,
|
|
scale: widget.extendedImageState.extendedImageInfo?.scale ?? 1.0,
|
|
color: widget.extendedImageState.imageWidget.color,
|
|
colorBlendMode: widget.extendedImageState.imageWidget.colorBlendMode,
|
|
fit: widget.extendedImageState.imageWidget.fit,
|
|
alignment: widget.extendedImageState.imageWidget.alignment,
|
|
repeat: widget.extendedImageState.imageWidget.repeat,
|
|
centerSlice: widget.extendedImageState.imageWidget.centerSlice,
|
|
matchTextDirection:
|
|
widget.extendedImageState.imageWidget.matchTextDirection,
|
|
invertColors: widget.extendedImageState.invertColors,
|
|
filterQuality: widget.extendedImageState.imageWidget.filterQuality,
|
|
beforePaintImage: widget.extendedImageState.imageWidget.beforePaintImage,
|
|
afterPaintImage: widget.extendedImageState.imageWidget.afterPaintImage,
|
|
gestureDetails: _gestureDetails,
|
|
);
|
|
|
|
if (extendedImageSlidePageState != null) {
|
|
image = widget.extendedImageState.imageWidget.heroBuilderForSlidingPage
|
|
?.call(image) ??
|
|
image;
|
|
if (extendedImageSlidePageState!.widget.slideType ==
|
|
SlideType.onlyImage) {
|
|
image = Transform.translate(
|
|
offset: extendedImageSlidePageState!.offset,
|
|
child: Transform.scale(
|
|
scale: extendedImageSlidePageState!.scale,
|
|
child: image,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
image = widget.imageBuilder?.call(image) ?? image;
|
|
|
|
image = GestureDetector(
|
|
onScaleStart: handleScaleStart,
|
|
onScaleUpdate: handleScaleUpdate,
|
|
onScaleEnd: handleScaleEnd,
|
|
onDoubleTap: _handleDoubleTap,
|
|
child: image,
|
|
behavior: _gestureConfig?.hitTestBehavior,
|
|
);
|
|
|
|
image = Listener(
|
|
child: image,
|
|
onPointerDown: _handlePointerDown,
|
|
onPointerSignal: _handlePointerSignal,
|
|
behavior: _gestureConfig!.hitTestBehavior,
|
|
);
|
|
|
|
return image;
|
|
}
|
|
|
|
@override
|
|
void didChangeDependencies() {
|
|
super.didChangeDependencies();
|
|
_pageViewState = null;
|
|
if (_gestureConfig!.inPageView) {
|
|
_pageViewState =
|
|
context.findAncestorStateOfType<ExtendedImageGesturePageViewState>();
|
|
_pageViewState?.extendedImageGestureState = this;
|
|
}
|
|
}
|
|
|
|
@override
|
|
void didUpdateWidget(GesturedImage oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
_initGestureConfig();
|
|
_pageViewState = null;
|
|
if (_gestureConfig!.inPageView) {
|
|
_pageViewState =
|
|
context.findAncestorStateOfType<ExtendedImageGesturePageViewState>();
|
|
_pageViewState?.extendedImageGestureState = this;
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_gestureAnimation.stop();
|
|
_gestureAnimation.dispose();
|
|
_pageViewState?.extendedImageGestureStates.remove(this);
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
void handleDoubleTap({double? scale, Offset? doubleTapPosition}) {
|
|
doubleTapPosition ??= _pointerDownPosition;
|
|
scale ??= _gestureConfig!.initialScale;
|
|
//scale = scale.clamp(_gestureConfig.minScale, _gestureConfig.maxScale);
|
|
handleScaleStart(ScaleStartDetails(focalPoint: doubleTapPosition!));
|
|
handleScaleUpdate(ScaleUpdateDetails(
|
|
focalPoint: doubleTapPosition,
|
|
scale: scale / _startingScale!,
|
|
focalPointDelta: Offset.zero,
|
|
));
|
|
if (scale < _gestureConfig!.minScale || scale > _gestureConfig!.maxScale) {
|
|
handleScaleEnd(ScaleEndDetails());
|
|
}
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_initGestureConfig();
|
|
}
|
|
|
|
@override
|
|
void reset() {
|
|
_gestureConfig = widget
|
|
.extendedImageState.imageWidget.initGestureConfigHandler
|
|
?.call(widget.extendedImageState) ??
|
|
GestureConfig();
|
|
|
|
gestureDetails = GestureDetails(
|
|
totalScale: _gestureConfig!.initialScale,
|
|
offset: Offset.zero,
|
|
initialAlignment: _gestureConfig!.initialAlignment,
|
|
);
|
|
}
|
|
|
|
@override
|
|
void slide() {
|
|
if (mounted) {
|
|
setState(() {
|
|
_gestureDetails!.slidePageOffset = extendedImageSlidePageState?.offset;
|
|
});
|
|
}
|
|
}
|
|
|
|
void _handleDoubleTap() {
|
|
if (widget.extendedImageState.imageWidget.onDoubleTap != null) {
|
|
widget.extendedImageState.imageWidget.onDoubleTap!(this);
|
|
return;
|
|
}
|
|
|
|
if (!mounted) {
|
|
return;
|
|
}
|
|
|
|
gestureDetails = GestureDetails(
|
|
offset: Offset.zero,
|
|
totalScale: _gestureConfig!.initialScale,
|
|
);
|
|
}
|
|
|
|
void _handlePointerDown(PointerDownEvent pointerDownEvent) {
|
|
_pointerDownPosition = pointerDownEvent.position;
|
|
_gestureAnimation.stop();
|
|
|
|
_pageViewState?.extendedImageGestureState = this;
|
|
}
|
|
|
|
void _handlePointerSignal(PointerSignalEvent event) {
|
|
if (event is PointerScrollEvent && event.kind == PointerDeviceKind.mouse) {
|
|
handleScaleStart(ScaleStartDetails(focalPoint: event.position));
|
|
final double dy = event.scrollDelta.dy;
|
|
final double dx = event.scrollDelta.dx;
|
|
handleScaleUpdate(ScaleUpdateDetails(
|
|
focalPoint: event.position,
|
|
scale: 1.0 +
|
|
_reverseIf((dy.abs() > dx.abs() ? dy : dx) *
|
|
_gestureConfig!.speed /
|
|
1000.0),
|
|
focalPointDelta: Offset.zero));
|
|
handleScaleEnd(ScaleEndDetails());
|
|
}
|
|
}
|
|
|
|
@override
|
|
void handleScaleEnd(ScaleEndDetails details) {
|
|
if (extendedImageSlidePageState != null &&
|
|
extendedImageSlidePageState!.isSliding) {
|
|
extendedImageSlidePageState!.endSlide(details);
|
|
return;
|
|
}
|
|
|
|
if (_pageViewState != null && _pageViewState!.isDraging) {
|
|
_pageViewState!.onDragEnd(DragEndDetails(
|
|
velocity: details.velocity,
|
|
primaryVelocity:
|
|
_pageViewState!.widget.scrollDirection == Axis.horizontal
|
|
? details.velocity.pixelsPerSecond.dx
|
|
: details.velocity.pixelsPerSecond.dy,
|
|
));
|
|
return;
|
|
}
|
|
|
|
//animate back to maxScale if gesture exceeded the maxScale specified
|
|
if (_gestureDetails!.totalScale!.greaterThan(_gestureConfig!.maxScale)) {
|
|
final double velocity =
|
|
(_gestureDetails!.totalScale! - _gestureConfig!.maxScale) /
|
|
_gestureConfig!.maxScale;
|
|
|
|
_gestureAnimation.animationScale(
|
|
_gestureDetails!.totalScale, _gestureConfig!.maxScale, velocity);
|
|
return;
|
|
}
|
|
|
|
//animate back to minScale if gesture fell smaller than the minScale specified
|
|
if (_gestureDetails!.totalScale!.lessThan(_gestureConfig!.minScale)) {
|
|
final double velocity =
|
|
(_gestureConfig!.minScale - _gestureDetails!.totalScale!) /
|
|
_gestureConfig!.minScale;
|
|
|
|
_gestureAnimation.animationScale(
|
|
_gestureDetails!.totalScale, _gestureConfig!.minScale, velocity);
|
|
return;
|
|
}
|
|
|
|
if (_gestureDetails!.actionType == ActionType.pan) {
|
|
// get magnitude from gesture velocity
|
|
final double magnitude = details.velocity.pixelsPerSecond.distance;
|
|
|
|
// do a significant magnitude
|
|
if (magnitude.greaterThanOrEqualTo(minMagnitude)) {
|
|
final Offset direction = details.velocity.pixelsPerSecond /
|
|
magnitude *
|
|
_gestureConfig!.inertialSpeed;
|
|
|
|
_gestureAnimation.animationOffset(
|
|
_gestureDetails!.offset, _gestureDetails!.offset! + direction);
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
void handleScaleStart(ScaleStartDetails details) {
|
|
_gestureAnimation.stop();
|
|
_normalizedOffset = (details.focalPoint - _gestureDetails!.offset!) /
|
|
_gestureDetails!.totalScale!;
|
|
_startingScale = _gestureDetails!.totalScale;
|
|
_startingOffset = details.focalPoint;
|
|
}
|
|
|
|
@override
|
|
void handleScaleUpdate(ScaleUpdateDetails details) {
|
|
// 取消原组件对totalScale的判断。这样scale大于1也能执行slidePage的slide方法
|
|
if (extendedImageSlidePageState != null &&
|
|
_gestureDetails!.userOffset &&
|
|
_gestureDetails!.actionType == ActionType.pan) {
|
|
final Offset totalDelta = details.focalPointDelta;
|
|
bool updateGesture = false;
|
|
if (!extendedImageSlidePageState!.isSliding) {
|
|
if (totalDelta.dx != 0 &&
|
|
totalDelta.dx.abs().greaterThan(totalDelta.dy.abs())) {
|
|
if (_gestureDetails!.computeHorizontalBoundary) {
|
|
if (totalDelta.dx > 0) {
|
|
updateGesture = _gestureDetails!.boundary.left;
|
|
} else {
|
|
updateGesture = _gestureDetails!.boundary.right;
|
|
}
|
|
} else {
|
|
updateGesture = true;
|
|
}
|
|
}
|
|
if (totalDelta.dy != 0 &&
|
|
totalDelta.dy.abs().greaterThan(totalDelta.dx.abs())) {
|
|
if (_gestureDetails!.computeVerticalBoundary) {
|
|
if (totalDelta.dy < 0) {
|
|
updateGesture = _gestureDetails!.boundary.bottom;
|
|
} else {
|
|
updateGesture = _gestureDetails!.boundary.top;
|
|
}
|
|
} else {
|
|
updateGesture = true;
|
|
}
|
|
}
|
|
} else {
|
|
updateGesture = true;
|
|
}
|
|
|
|
if (details.focalPointDelta.distance.greaterThan(minGesturePageDelta) &&
|
|
updateGesture) {
|
|
extendedImageSlidePageState!.slide(
|
|
details.focalPointDelta,
|
|
extendedImageGestureState: this,
|
|
);
|
|
}
|
|
}
|
|
|
|
if (extendedImageSlidePageState != null &&
|
|
extendedImageSlidePageState!.isSliding) {
|
|
return;
|
|
}
|
|
|
|
// totalScale > 1 and page view is starting to move
|
|
if (_pageViewState != null) {
|
|
final ExtendedImageGesturePageViewState pageViewState = _pageViewState!;
|
|
|
|
final Axis axis = pageViewState.widget.scrollDirection;
|
|
final bool movePage = _pageViewState!.isDraging ||
|
|
(details.pointerCount == 1 &&
|
|
details.scale == 1 &&
|
|
_gestureDetails!.movePage(details.focalPointDelta, axis));
|
|
|
|
if (movePage) {
|
|
if (!pageViewState.isDraging) {
|
|
pageViewState
|
|
.onDragDown(DragDownDetails(globalPosition: details.focalPoint));
|
|
pageViewState.onDragStart(
|
|
DragStartDetails(globalPosition: details.focalPoint));
|
|
//assert(!pageViewState.isDraging);
|
|
}
|
|
Offset delta = details.focalPointDelta;
|
|
delta =
|
|
axis == Axis.horizontal ? Offset(delta.dx, 0) : Offset(0, delta.dy);
|
|
|
|
pageViewState.onDragUpdate(DragUpdateDetails(
|
|
globalPosition: details.focalPoint,
|
|
delta: delta,
|
|
primaryDelta: axis == Axis.horizontal ? delta.dx : delta.dy,
|
|
));
|
|
|
|
return;
|
|
}
|
|
}
|
|
final double? scale = widget.canScaleImage(_gestureDetails)
|
|
? clampScale(
|
|
_startingScale! * details.scale * _gestureConfig!.speed,
|
|
_gestureConfig!.animationMinScale,
|
|
_gestureConfig!.animationMaxScale)
|
|
: _gestureDetails!.totalScale;
|
|
|
|
//Round the scale to three points after comma to prevent shaking
|
|
//scale = roundAfter(scale, 3);
|
|
//no more zoom
|
|
if (details.scale != 1.0 &&
|
|
((_gestureDetails!.totalScale!
|
|
.equalTo(_gestureConfig!.animationMinScale) &&
|
|
scale!.lessThanOrEqualTo(_gestureDetails!.totalScale!)) ||
|
|
(_gestureDetails!.totalScale!
|
|
.equalTo(_gestureConfig!.animationMaxScale) &&
|
|
scale!.greaterThanOrEqualTo(_gestureDetails!.totalScale!)))) {
|
|
return;
|
|
}
|
|
|
|
final Offset offset = (details.scale == 1.0
|
|
? details.focalPoint * _gestureConfig!.speed
|
|
: _startingOffset) -
|
|
_normalizedOffset * scale!;
|
|
|
|
if (mounted &&
|
|
(offset != _gestureDetails!.offset ||
|
|
scale != _gestureDetails!.totalScale)) {
|
|
gestureDetails = GestureDetails(
|
|
offset: offset,
|
|
totalScale: scale,
|
|
gestureDetails: _gestureDetails,
|
|
actionType: details.scale != 1.0 ? ActionType.zoom : ActionType.pan);
|
|
}
|
|
}
|
|
|
|
void _initGestureConfig() {
|
|
final double? initialScale = _gestureConfig?.initialScale;
|
|
final InitialAlignment? initialAlignment = _gestureConfig?.initialAlignment;
|
|
_gestureConfig = widget
|
|
.extendedImageState.imageWidget.initGestureConfigHandler
|
|
?.call(widget.extendedImageState) ??
|
|
GestureConfig();
|
|
|
|
if (_gestureDetails == null ||
|
|
initialScale != _gestureConfig!.initialScale ||
|
|
initialAlignment != _gestureConfig!.initialAlignment) {
|
|
_gestureDetails = GestureDetails(
|
|
totalScale: _gestureConfig!.initialScale,
|
|
offset: Offset.zero,
|
|
initialAlignment: _gestureConfig!.initialAlignment,
|
|
);
|
|
}
|
|
|
|
if (_gestureConfig!.cacheGesture) {
|
|
final GestureDetails? cache =
|
|
_gestureDetailsCache[widget.extendedImageState.imageStreamKey];
|
|
if (cache != null) {
|
|
_gestureDetails = cache;
|
|
}
|
|
}
|
|
_gestureDetails ??= GestureDetails(
|
|
totalScale: _gestureConfig!.initialScale,
|
|
offset: Offset.zero,
|
|
);
|
|
|
|
_gestureAnimation = GestureAnimation(this, offsetCallBack: (Offset value) {
|
|
gestureDetails = GestureDetails(
|
|
offset: value,
|
|
totalScale: _gestureDetails!.totalScale,
|
|
gestureDetails: _gestureDetails);
|
|
}, scaleCallBack: (double scale) {
|
|
gestureDetails = GestureDetails(
|
|
offset: _gestureDetails!.offset,
|
|
totalScale: scale,
|
|
gestureDetails: _gestureDetails,
|
|
actionType: ActionType.zoom,
|
|
userOffset: false);
|
|
});
|
|
}
|
|
|
|
double _reverseIf(double scaleDetal) {
|
|
if (_gestureConfig?.reverseMousePointerScrollDirection ?? false) {
|
|
return -scaleDetal;
|
|
} else {
|
|
return scaleDetal;
|
|
}
|
|
}
|
|
}
|