import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:extended_image/extended_image.dart'; Map _gestureDetailsCache = {}; ///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(); _pageViewState?.extendedImageGestureState = this; } } @override void didUpdateWidget(GesturedImage oldWidget) { super.didUpdateWidget(oldWidget); _initGestureConfig(); _pageViewState = null; if (_gestureConfig!.inPageView) { _pageViewState = context.findAncestorStateOfType(); _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; } } }