aves/lib/widgets/common/magnifier/controller/controller_delegate.dart

226 lines
8.2 KiB
Dart

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/scale_level.dart';
import 'package:aves/widgets/common/magnifier/scale/scalestate_controller.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<MagnifierCore> {
MagnifierController get controller => widget.controller;
MagnifierScaleStateController get scaleStateController => widget.scaleStateController;
ScaleBoundaries get scaleBoundaries => widget.scaleBoundaries;
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<StreamSubscription> _streamSubs = [];
void startListeners() {
_streamSubs.add(controller.outputStateStream.listen(_onMagnifierStateChange));
_streamSubs.add(scaleStateController.scaleStateChangeStream.listen(_onScaleStateChange));
}
void _onScaleStateChange(ScaleStateChange scaleStateChange) {
if (scaleStateChange.source == ChangeSource.internal) return;
if (!scaleStateController.hasChanged) return;
if (_animateScale == null || scaleStateController.isZooming) {
controller.setScale(scale, scaleStateChange.source);
return;
}
final nextScaleState = scaleStateChange.state;
final nextScale = getScaleForScaleState(nextScaleState, scaleBoundaries);
var nextPosition = Offset.zero;
if (nextScaleState == ScaleState.covering || nextScaleState == ScaleState.originalSize) {
final childFocalPoint = scaleStateChange.childFocalPoint;
if (childFocalPoint != null) {
final childCenter = scaleBoundaries.childSize.center(Offset.zero);
nextPosition = (childCenter - childFocalPoint) * nextScale;
}
}
final prevScale = controller.scale ?? getScaleForScaleState(scaleStateController.prevScaleState.state, scaleBoundaries);
_animateScale(prevScale, nextScale, nextPosition);
}
void addAnimateOnScaleStateUpdate(void Function(double prevScale, double nextScale, Offset nextPosition) animateScale) {
_animateScale = animateScale;
}
void _onMagnifierStateChange(MagnifierState state) {
controller.setPosition(clampPosition(), state.source);
if (controller.scale == controller.prevValue.scale) return;
if (state.source == ChangeSource.internal || state.source == ChangeSource.animation) return;
final newScaleState = (scale > scaleBoundaries.initialScale) ? ScaleState.zoomedIn : ScaleState.zoomedOut;
scaleStateController.setScaleState(newScaleState, state.source);
}
Offset get position => controller.position;
double get scale {
final scaleState = scaleStateController.scaleState.state;
final needsRecalc = markNeedsScaleRecalc && !(scaleState == ScaleState.zoomedIn || scaleState == ScaleState.zoomedOut);
final scaleExistsOnController = controller.scale != null;
if (needsRecalc || !scaleExistsOnController) {
final newScale = getScaleForScaleState(scaleState, scaleBoundaries);
markNeedsScaleRecalc = false;
setScale(newScale, ChangeSource.internal);
return newScale;
}
return controller.scale;
}
void setScale(double scale, ChangeSource source) => controller.setScale(scale, source);
void updateMultiple({
Offset position,
double scale,
@required ChangeSource source,
}) {
controller.updateMultiple(position: position, scale: scale, source: source);
}
void updateScaleStateFromNewScale(double newScale, ChangeSource source) {
// debugPrint('updateScaleStateFromNewScale scale=$newScale, source=$source');
var newScaleState = ScaleState.initial;
if (scale != scaleBoundaries.initialScale) {
newScaleState = (newScale > scaleBoundaries.initialScale) ? ScaleState.zoomedIn : ScaleState.zoomedOut;
}
scaleStateController.setScaleState(newScaleState, source);
}
void nextScaleState(ChangeSource source, {Offset childFocalPoint}) {
// debugPrint('$runtimeType nextScaleState source=$source');
final scaleState = scaleStateController.scaleState.state;
if (scaleState == ScaleState.zoomedIn || scaleState == ScaleState.zoomedOut) {
scaleStateController.setScaleState(scaleStateCycle(scaleState), source, childFocalPoint: childFocalPoint);
return;
}
final originalScale = getScaleForScaleState(
scaleState,
scaleBoundaries,
);
var prevScale = originalScale;
var prevScaleState = scaleState;
var nextScale = originalScale;
var nextScaleState = scaleState;
do {
prevScale = nextScale;
prevScaleState = nextScaleState;
nextScaleState = scaleStateCycle(prevScaleState);
nextScale = getScaleForScaleState(nextScaleState, scaleBoundaries);
} while (prevScale == nextScale && scaleState != nextScaleState);
if (originalScale == nextScale) return;
scaleStateController.setScaleState(nextScaleState, source, childFocalPoint: childFocalPoint);
}
CornersRange cornersX({double scale}) {
final _scale = scale ?? this.scale;
final computedWidth = scaleBoundaries.childSize.width * _scale;
final screenWidth = scaleBoundaries.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 = scaleBoundaries.childSize.height * _scale;
final screenHeight = scaleBoundaries.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 = scaleBoundaries.childSize.width * _scale;
final computedHeight = scaleBoundaries.childSize.height * _scale;
final screenWidth = scaleBoundaries.viewportSize.width;
final screenHeight = scaleBoundaries.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;
_streamSubs.forEach((sub) => sub.cancel());
_streamSubs.clear();
super.dispose();
}
double getScaleForScaleState(
ScaleState scaleState,
ScaleBoundaries scaleBoundaries,
) {
double _clamp(double scale, ScaleBoundaries boundaries) => scale.clamp(boundaries.minScale, boundaries.maxScale);
switch (scaleState) {
case ScaleState.initial:
case ScaleState.zoomedIn:
case ScaleState.zoomedOut:
return _clamp(scaleBoundaries.initialScale, scaleBoundaries);
case ScaleState.covering:
return _clamp(ScaleLevel.scaleForCovering(scaleBoundaries.viewportSize, scaleBoundaries.childSize), scaleBoundaries);
case ScaleState.originalSize:
return _clamp(1.0, scaleBoundaries);
default:
return null;
}
}
}
/// Simple class to store a min and a max value
class CornersRange {
const CornersRange(this.min, this.max);
final double min;
final double max;
}