import 'dart:async'; import 'dart:ui'; import 'package:aves/widgets/common/magnifier/controller/controller.dart'; import 'package:aves/widgets/common/magnifier/controller/state.dart'; import 'package:aves/widgets/common/magnifier/core/core.dart'; import 'package:aves/widgets/common/magnifier/scale/scale_boundaries.dart'; import 'package:aves/widgets/common/magnifier/scale/state.dart'; import 'package:flutter/widgets.dart'; /// A class to hold internal layout logic to sync both controller states /// /// It reacts to layout changes (eg: enter landscape or widget resize) and syncs the two controllers. mixin MagnifierControllerDelegate on State { MagnifierController get controller => widget.controller; ScaleBoundaries get scaleBoundaries => controller.scaleBoundaries; Size get childSize => scaleBoundaries.childSize; Size get viewportSize => scaleBoundaries.viewportSize; ScaleStateCycle get scaleStateCycle => widget.scaleStateCycle; Alignment get basePosition => Alignment.center; Function(double? prevScale, double? nextScale, Offset nextPosition)? _animateScale; /// Mark if scale need recalculation, useful for scale boundaries changes. bool markNeedsScaleRecalc = true; final List _subscriptions = []; void startListeners() { _subscriptions.add(controller.stateStream.listen(_onMagnifierStateChange)); _subscriptions.add(controller.scaleStateChangeStream.listen(_onScaleStateChange)); } void _onScaleStateChange(ScaleStateChange scaleStateChange) { if (scaleStateChange.source == ChangeSource.internal) return; if (!controller.hasScaleSateChanged) return; if (_animateScale == null || controller.isZooming) { controller.update(scale: scale, source: scaleStateChange.source); return; } final nextScaleState = scaleStateChange.state; final nextScale = controller.getScaleForScaleState(nextScaleState); var nextPosition = Offset.zero; if (nextScaleState == ScaleState.covering || nextScaleState == ScaleState.originalSize) { final childFocalPoint = scaleStateChange.childFocalPoint; if (childFocalPoint != null) { nextPosition = scaleBoundaries.childToStatePosition(nextScale!, childFocalPoint); } } final prevScale = controller.scale ?? controller.getScaleForScaleState(controller.previousScaleState.state); _animateScale!(prevScale, nextScale, nextPosition); } void setScaleStateUpdateAnimation(void Function(double? prevScale, double? nextScale, Offset nextPosition) animateScale) { _animateScale = animateScale; } void _onMagnifierStateChange(MagnifierState state) { controller.update(position: clampPosition(), source: state.source); if (controller.scale == controller.previousState.scale) return; if (state.source == ChangeSource.internal || state.source == ChangeSource.animation) return; final newScaleState = (scale! > scaleBoundaries.initialScale) ? ScaleState.zoomedIn : ScaleState.zoomedOut; controller.setScaleState(newScaleState, state.source); } Offset get position => controller.position; double? get scale { final scaleState = controller.scaleState.state; final needsRecalc = markNeedsScaleRecalc && !(scaleState == ScaleState.zoomedIn || scaleState == ScaleState.zoomedOut); final scaleExistsOnController = controller.scale != null; if (needsRecalc || !scaleExistsOnController) { final newScale = controller.getScaleForScaleState(scaleState); markNeedsScaleRecalc = false; setScale(newScale, ChangeSource.internal); return newScale; } return controller.scale; } void setScale(double? scale, ChangeSource source) => controller.update(scale: scale, source: source); void updateMultiple({ required Offset position, required double scale, required ChangeSource source, }) { controller.update(position: position, scale: scale, source: source); } void updateScaleStateFromNewScale(double newScale, ChangeSource source) { var newScaleState = ScaleState.initial; if (scale != scaleBoundaries.initialScale) { newScaleState = (newScale > scaleBoundaries.initialScale) ? ScaleState.zoomedIn : ScaleState.zoomedOut; } controller.setScaleState(newScaleState, source); } void nextScaleState(ChangeSource source, {Offset? childFocalPoint}) { final scaleState = controller.scaleState.state; if (scaleState == ScaleState.zoomedIn || scaleState == ScaleState.zoomedOut) { controller.setScaleState(scaleStateCycle(scaleState), source, childFocalPoint: childFocalPoint); return; } final originalScale = controller.getScaleForScaleState(scaleState); var prevScale = originalScale; var prevScaleState = scaleState; var nextScale = originalScale; var nextScaleState = scaleState; do { prevScale = nextScale; prevScaleState = nextScaleState; nextScaleState = scaleStateCycle(prevScaleState); nextScale = controller.getScaleForScaleState(nextScaleState); } while (prevScale == nextScale && scaleState != nextScaleState); if (originalScale == nextScale) return; controller.setScaleState(nextScaleState, source, childFocalPoint: childFocalPoint); } CornersRange cornersX({double? scale}) { final _scale = scale ?? this.scale!; final computedWidth = childSize.width * _scale; final screenWidth = viewportSize.width; final positionX = basePosition.x; final widthDiff = computedWidth - screenWidth; final minX = ((positionX - 1).abs() / 2) * widthDiff * -1; final maxX = ((positionX + 1).abs() / 2) * widthDiff; return CornersRange(minX, maxX); } CornersRange cornersY({double? scale}) { final _scale = scale ?? this.scale!; final computedHeight = childSize.height * _scale; final screenHeight = viewportSize.height; final positionY = basePosition.y; final heightDiff = computedHeight - screenHeight; final minY = ((positionY - 1).abs() / 2) * heightDiff * -1; final maxY = ((positionY + 1).abs() / 2) * heightDiff; return CornersRange(minY, maxY); } Offset clampPosition({Offset? position, double? scale}) { final _scale = scale ?? this.scale!; final _position = position ?? this.position; final computedWidth = childSize.width * _scale; final computedHeight = childSize.height * _scale; final screenWidth = viewportSize.width; final screenHeight = viewportSize.height; var finalX = 0.0; if (screenWidth < computedWidth) { final cornersX = this.cornersX(scale: _scale); finalX = _position.dx.clamp(cornersX.min, cornersX.max); } var finalY = 0.0; if (screenHeight < computedHeight) { final cornersY = this.cornersY(scale: _scale); finalY = _position.dy.clamp(cornersY.min, cornersY.max); } return Offset(finalX, finalY); } @override void dispose() { _animateScale = null; _subscriptions.forEach((sub) => sub.cancel()); _subscriptions.clear(); super.dispose(); } } /// Simple class to store a min and a max value class CornersRange { const CornersRange(this.min, this.max); final double min; final double max; }