From b14558e4514cdc0d806cb4437874c3223eb51f8c Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Mon, 21 Dec 2020 20:11:14 +0900 Subject: [PATCH] svg: optional checkered background --- lib/model/settings/entry_background.dart | 26 +++ lib/model/settings/settings.dart | 7 +- .../collection/thumbnail/decorated.dart | 1 - lib/widgets/collection/thumbnail/vector.dart | 58 +++++-- .../common/fx/checkered_decoration.dart | 57 ++++++ .../common/fx/highlight_decoration.dart | 8 +- .../magnifier/controller/controller.dart | 162 ++++++++++-------- .../controller/controller_delegate.dart | 81 +++------ lib/widgets/common/magnifier/core/core.dart | 85 +++++---- lib/widgets/common/magnifier/magnifier.dart | 35 +--- .../scale/scalestate_controller.dart | 41 ----- lib/widgets/filter_grids/album_pick.dart | 8 +- lib/widgets/fullscreen/image_view.dart | 109 ++++++++---- .../info/metadata/metadata_section.dart | 8 +- lib/widgets/fullscreen/overlay/minimap.dart | 34 ++-- lib/widgets/fullscreen/tiled_view.dart | 37 ++-- lib/widgets/settings/svg_background.dart | 54 ++++-- 17 files changed, 443 insertions(+), 368 deletions(-) create mode 100644 lib/model/settings/entry_background.dart create mode 100644 lib/widgets/common/fx/checkered_decoration.dart delete mode 100644 lib/widgets/common/magnifier/scale/scalestate_controller.dart diff --git a/lib/model/settings/entry_background.dart b/lib/model/settings/entry_background.dart new file mode 100644 index 000000000..ee0ffe4c1 --- /dev/null +++ b/lib/model/settings/entry_background.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; + +enum EntryBackground { black, white, transparent, checkered } + +extension ExtraEntryBackground on EntryBackground { + bool get isColor { + switch (this) { + case EntryBackground.black: + case EntryBackground.white: + return true; + default: + return false; + } + } + + Color get color { + switch (this) { + case EntryBackground.black: + return Colors.black; + case EntryBackground.white: + return Colors.white; + default: + return null; + } + } +} diff --git a/lib/model/settings/settings.dart b/lib/model/settings/settings.dart index 87f6e0380..8ee13f644 100644 --- a/lib/model/settings/settings.dart +++ b/lib/model/settings/settings.dart @@ -1,5 +1,6 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/settings/coordinate_format.dart'; +import 'package:aves/model/settings/entry_background.dart'; import 'package:aves/model/settings/home_page.dart'; import 'package:aves/model/settings/screen_on.dart'; import 'package:aves/widgets/fullscreen/info/location_section.dart'; @@ -54,7 +55,7 @@ class Settings extends ChangeNotifier { static const coordinateFormatKey = 'coordinates_format'; // rendering - static const svgBackgroundKey = 'svg_background'; + static const vectorBackgroundKey = 'vector_background'; // search static const saveSearchHistoryKey = 'save_search_history'; @@ -184,9 +185,9 @@ class Settings extends ChangeNotifier { // rendering - int get svgBackground => _prefs.getInt(svgBackgroundKey) ?? 0xFFFFFFFF; + EntryBackground get vectorBackground => getEnumOrDefault(vectorBackgroundKey, EntryBackground.white, EntryBackground.values); - set svgBackground(int newValue) => setAndNotify(svgBackgroundKey, newValue); + set vectorBackground(EntryBackground newValue) => setAndNotify(vectorBackgroundKey, newValue.toString()); // search diff --git a/lib/widgets/collection/thumbnail/decorated.dart b/lib/widgets/collection/thumbnail/decorated.dart index 71dd0377c..a92d4e550 100644 --- a/lib/widgets/collection/thumbnail/decorated.dart +++ b/lib/widgets/collection/thumbnail/decorated.dart @@ -43,7 +43,6 @@ class DecoratedThumbnail extends StatelessWidget { ); child = Stack( - fit: StackFit.passthrough, children: [ child, Positioned( diff --git a/lib/widgets/collection/thumbnail/vector.dart b/lib/widgets/collection/thumbnail/vector.dart index ed238e02e..c589da062 100644 --- a/lib/widgets/collection/thumbnail/vector.dart +++ b/lib/widgets/collection/thumbnail/vector.dart @@ -1,6 +1,10 @@ -import 'package:aves/model/image_entry.dart'; -import 'package:aves/model/settings/settings.dart'; +import 'dart:math'; + import 'package:aves/image_providers/uri_picture_provider.dart'; +import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/settings/entry_background.dart'; +import 'package:aves/model/settings/settings.dart'; +import 'package:aves/widgets/common/fx/checkered_decoration.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:provider/provider.dart'; @@ -19,23 +23,39 @@ class ThumbnailVectorImage extends StatelessWidget { @override Widget build(BuildContext context) { - final child = Container( - // center `SvgPicture` inside `Container` with the thumbnail dimensions - // so that `SvgPicture` doesn't get aligned by the `Stack` like the overlay icons - width: extent, - height: extent, - child: Selector( - selector: (context, s) => s.svgBackground, - builder: (context, svgBackground, child) { - final colorFilter = ColorFilter.mode(Color(svgBackground), BlendMode.dstOver); - return SvgPicture( - UriPicture( - uri: entry.uri, - mimeType: entry.mimeType, - colorFilter: colorFilter, - ), - width: extent, - height: extent, + final pictureProvider = UriPicture( + uri: entry.uri, + mimeType: entry.mimeType, + ); + + final child = Center( + child: Selector( + selector: (context, s) => s.vectorBackground, + builder: (context, background, child) { + if (background == EntryBackground.transparent) { + return SvgPicture( + pictureProvider, + width: extent, + height: extent, + ); + } + + final longestSide = max(entry.width, entry.height); + final picture = SvgPicture( + pictureProvider, + width: extent * (entry.width / longestSide), + height: extent * (entry.height / longestSide), + ); + + Decoration decoration; + if (background == EntryBackground.checkered) { + decoration = CheckeredDecoration(checkSize: extent / 8); + } else if (background.isColor) { + decoration = BoxDecoration(color: background.color); + } + return DecoratedBox( + decoration: decoration, + child: picture, ); }, ), diff --git a/lib/widgets/common/fx/checkered_decoration.dart b/lib/widgets/common/fx/checkered_decoration.dart new file mode 100644 index 000000000..4d541eaee --- /dev/null +++ b/lib/widgets/common/fx/checkered_decoration.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; + +class CheckeredDecoration extends Decoration { + final Color light, dark; + final double checkSize; + final Offset offset; + + const CheckeredDecoration({ + this.light = const Color(0xFF999999), + this.dark = const Color(0xFF666666), + this.checkSize = 20, + this.offset = Offset.zero, + }); + + @override + _CheckeredDecorationPainter createBoxPainter([VoidCallback onChanged]) { + return _CheckeredDecorationPainter(this, onChanged); + } +} + +class _CheckeredDecorationPainter extends BoxPainter { + final CheckeredDecoration decoration; + + const _CheckeredDecorationPainter(this.decoration, VoidCallback onChanged) : super(onChanged); + + @override + void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) { + final size = configuration.size; + var dx = offset.dx; + var dy = offset.dy; + + final lightPaint = Paint()..color = decoration.light; + final darkPaint = Paint()..color = decoration.dark; + final checkSize = decoration.checkSize; + + // save/restore because of the clip + canvas.save(); + canvas.clipRect(Rect.fromLTWH(dx, dy, size.width, size.height)); + + canvas.drawPaint(lightPaint); + + dx += decoration.offset.dx % (decoration.checkSize * 2); + dy += decoration.offset.dy % (decoration.checkSize * 2); + + final xMax = size.width / checkSize; + final yMax = size.height / checkSize; + for (var x = -2; x < xMax; x++) { + for (var y = -2; y < yMax; y++) { + if ((x + y) % 2 == 0) { + final rect = Rect.fromLTWH(dx + x * checkSize, dy + y * checkSize, checkSize, checkSize); + canvas.drawRect(rect, darkPaint); + } + } + } + canvas.restore(); + } +} diff --git a/lib/widgets/common/fx/highlight_decoration.dart b/lib/widgets/common/fx/highlight_decoration.dart index 123e186a4..096d139cd 100644 --- a/lib/widgets/common/fx/highlight_decoration.dart +++ b/lib/widgets/common/fx/highlight_decoration.dart @@ -6,15 +6,15 @@ class HighlightDecoration extends Decoration { const HighlightDecoration({@required this.color}); @override - HighlightDecorationPainter createBoxPainter([VoidCallback onChanged]) { - return HighlightDecorationPainter(this, onChanged); + _HighlightDecorationPainter createBoxPainter([VoidCallback onChanged]) { + return _HighlightDecorationPainter(this, onChanged); } } -class HighlightDecorationPainter extends BoxPainter { +class _HighlightDecorationPainter extends BoxPainter { final HighlightDecoration decoration; - const HighlightDecorationPainter(this.decoration, VoidCallback onChanged) : super(onChanged); + const _HighlightDecorationPainter(this.decoration, VoidCallback onChanged) : super(onChanged); @override void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) { diff --git a/lib/widgets/common/magnifier/controller/controller.dart b/lib/widgets/common/magnifier/controller/controller.dart index c33a27595..d4c5b37f9 100644 --- a/lib/widgets/common/magnifier/controller/controller.dart +++ b/lib/widgets/common/magnifier/controller/controller.dart @@ -2,101 +2,127 @@ import 'dart:async'; import 'dart:ui'; import 'package:aves/widgets/common/magnifier/controller/state.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/state.dart'; import 'package:flutter/widgets.dart'; class MagnifierController { + final StreamController _stateStreamController = StreamController.broadcast(); + final StreamController _scaleBoundariesStreamController = StreamController.broadcast(); + final StreamController _scaleStateChangeStreamController = StreamController.broadcast(); + + MagnifierState _currentState, initial, previousState; + ScaleBoundaries _scaleBoundaries; + ScaleStateChange _currentScaleState, previousScaleState; + MagnifierController({ Offset initialPosition = Offset.zero, - }) : _valueNotifier = ValueNotifier( - MagnifierState( - position: initialPosition, - scale: null, - source: ChangeSource.internal, - ), - ), - super() { - initial = value; - prevValue = initial; + }) : super() { + initial = MagnifierState( + position: initialPosition, + scale: null, + source: ChangeSource.internal, + ); + previousState = initial; + _setState(initial); - _valueNotifier.addListener(_changeListener); - _outputCtrl = StreamController.broadcast(); - _outputCtrl.sink.add(initial); + final _initialScaleState = ScaleStateChange(state: ScaleState.initial, source: ChangeSource.internal); + previousScaleState = _initialScaleState; + _setScaleState(_initialScaleState); } - final ValueNotifier _valueNotifier; + Stream get stateStream => _stateStreamController.stream; - MagnifierState initial; + Stream get scaleBoundariesStream => _scaleBoundariesStreamController.stream; - StreamController _outputCtrl; + Stream get scaleStateChangeStream => _scaleStateChangeStreamController.stream; - /// The output for state/value updates. Usually a broadcast [Stream] - Stream get outputStateStream => _outputCtrl.stream; + MagnifierState get currentState => _currentState; - /// The state value before the last change or the initial state if the state has not been changed. - MagnifierState prevValue; + Offset get position => currentState.position; - /// Resets the state to the initial value; - void reset() { - _setValue(initial); - } + double get scale => currentState.scale; - void _changeListener() { - _outputCtrl.sink.add(value); - } + ScaleBoundaries get scaleBoundaries => _scaleBoundaries; + + ScaleStateChange get scaleState => _currentScaleState; + + bool get hasScaleSateChanged => previousScaleState != scaleState; + + bool get isZooming => scaleState.state == ScaleState.zoomedIn || scaleState.state == ScaleState.zoomedOut; /// Closes streams and removes eventual listeners. void dispose() { - _outputCtrl.close(); - _valueNotifier.dispose(); + _stateStreamController.close(); + _scaleBoundariesStreamController.close(); + _scaleStateChangeStreamController.close(); } - void setPosition(Offset position, ChangeSource source) { - if (value.position == position) return; - - prevValue = value; - _setValue(MagnifierState( - position: position, - scale: scale, - source: source, - )); - } - - /// The position of the image in the screen given its offset after pan gestures. - Offset get position => value.position; - - void setScale(double scale, ChangeSource source) { - if (value.scale == scale) return; - - prevValue = value; - _setValue(MagnifierState( - position: position, - scale: scale, - source: source, - )); - } - - /// The scale factor to transform the child (image or a customChild). - double get scale => value.scale; - - /// Update multiple fields of the state with only one update streamed. - void updateMultiple({ + void update({ Offset position, double scale, @required ChangeSource source, }) { - prevValue = value; - _setValue(MagnifierState( - position: position ?? value.position, - scale: scale ?? value.scale, + position = position ?? this.position; + scale = scale ?? this.scale; + if (this.position == position && this.scale == scale) return; + + previousState = currentState; + _setState(MagnifierState( + position: position, + scale: scale, source: source, )); } - /// The actual state value - MagnifierState get value => _valueNotifier.value; + void setScaleState(ScaleState newValue, ChangeSource source, {Offset childFocalPoint}) { + if (_currentScaleState.state == newValue) return; - void _setValue(MagnifierState newValue) { - if (_valueNotifier.value == newValue) return; - _valueNotifier.value = newValue; + previousScaleState = _currentScaleState; + _currentScaleState = ScaleStateChange(state: newValue, source: source, childFocalPoint: childFocalPoint); + _scaleStateChangeStreamController.sink.add(scaleState); + } + + void _setState(MagnifierState state) { + if (_currentState == state) return; + _currentState = state; + _stateStreamController.sink.add(state); + } + + void setScaleBoundaries(ScaleBoundaries scaleBoundaries) { + if (_scaleBoundaries == scaleBoundaries) return; + _scaleBoundaries = scaleBoundaries; + _scaleBoundariesStreamController.sink.add(scaleBoundaries); + + if (!isZooming) { + update( + scale: getScaleForScaleState(_currentScaleState.state), + source: ChangeSource.internal, + ); + } + } + + void _setScaleState(ScaleStateChange scaleState) { + if (_currentScaleState == scaleState) return; + _currentScaleState = scaleState; + _scaleStateChangeStreamController.sink.add(_currentScaleState); + } + + double getScaleForScaleState(ScaleState scaleState) { + 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; + } } } diff --git a/lib/widgets/common/magnifier/controller/controller_delegate.dart b/lib/widgets/common/magnifier/controller/controller_delegate.dart index 5a3455e49..4169dced5 100644 --- a/lib/widgets/common/magnifier/controller/controller_delegate.dart +++ b/lib/widgets/common/magnifier/controller/controller_delegate.dart @@ -5,8 +5,6 @@ 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'; @@ -16,9 +14,7 @@ import 'package:flutter/widgets.dart'; mixin MagnifierControllerDelegate on State { MagnifierController get controller => widget.controller; - MagnifierScaleStateController get scaleStateController => widget.scaleStateController; - - ScaleBoundaries get scaleBoundaries => widget.scaleBoundaries; + ScaleBoundaries get scaleBoundaries => controller.scaleBoundaries; ScaleStateCycle get scaleStateCycle => widget.scaleStateCycle; @@ -29,24 +25,24 @@ mixin MagnifierControllerDelegate on State { /// Mark if scale need recalculation, useful for scale boundaries changes. bool markNeedsScaleRecalc = true; - final List _streamSubs = []; + final List _subscriptions = []; void startListeners() { - _streamSubs.add(controller.outputStateStream.listen(_onMagnifierStateChange)); - _streamSubs.add(scaleStateController.scaleStateChangeStream.listen(_onScaleStateChange)); + _subscriptions.add(controller.stateStream.listen(_onMagnifierStateChange)); + _subscriptions.add(controller.scaleStateChangeStream.listen(_onScaleStateChange)); } void _onScaleStateChange(ScaleStateChange scaleStateChange) { if (scaleStateChange.source == ChangeSource.internal) return; - if (!scaleStateController.hasChanged) return; + if (!controller.hasScaleSateChanged) return; - if (_animateScale == null || scaleStateController.isZooming) { - controller.setScale(scale, scaleStateChange.source); + if (_animateScale == null || controller.isZooming) { + controller.update(scale: scale, source: scaleStateChange.source); return; } final nextScaleState = scaleStateChange.state; - final nextScale = getScaleForScaleState(nextScaleState, scaleBoundaries); + final nextScale = controller.getScaleForScaleState(nextScaleState); var nextPosition = Offset.zero; if (nextScaleState == ScaleState.covering || nextScaleState == ScaleState.originalSize) { final childFocalPoint = scaleStateChange.childFocalPoint; @@ -55,31 +51,31 @@ mixin MagnifierControllerDelegate on State { } } - final prevScale = controller.scale ?? getScaleForScaleState(scaleStateController.prevScaleState.state, scaleBoundaries); + final prevScale = controller.scale ?? controller.getScaleForScaleState(controller.previousScaleState.state); _animateScale(prevScale, nextScale, nextPosition); } - void addAnimateOnScaleStateUpdate(void Function(double prevScale, double nextScale, Offset nextPosition) animateScale) { + void setScaleStateUpdateAnimation(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; + 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; - scaleStateController.setScaleState(newScaleState, state.source); + controller.setScaleState(newScaleState, state.source); } Offset get position => controller.position; double get scale { - final scaleState = scaleStateController.scaleState.state; + final scaleState = controller.scaleState.state; final needsRecalc = markNeedsScaleRecalc && !(scaleState == ScaleState.zoomedIn || scaleState == ScaleState.zoomedOut); final scaleExistsOnController = controller.scale != null; if (needsRecalc || !scaleExistsOnController) { - final newScale = getScaleForScaleState(scaleState, scaleBoundaries); + final newScale = controller.getScaleForScaleState(scaleState); markNeedsScaleRecalc = false; setScale(newScale, ChangeSource.internal); return newScale; @@ -87,14 +83,14 @@ mixin MagnifierControllerDelegate on State { return controller.scale; } - void setScale(double scale, ChangeSource source) => controller.setScale(scale, source); + void setScale(double scale, ChangeSource source) => controller.update(scale: scale, source: source); void updateMultiple({ - Offset position, - double scale, + @required Offset position, + @required double scale, @required ChangeSource source, }) { - controller.updateMultiple(position: position, scale: scale, source: source); + controller.update(position: position, scale: scale, source: source); } void updateScaleStateFromNewScale(double newScale, ChangeSource source) { @@ -102,19 +98,16 @@ mixin MagnifierControllerDelegate on State { if (scale != scaleBoundaries.initialScale) { newScaleState = (newScale > scaleBoundaries.initialScale) ? ScaleState.zoomedIn : ScaleState.zoomedOut; } - scaleStateController.setScaleState(newScaleState, source); + controller.setScaleState(newScaleState, source); } void nextScaleState(ChangeSource source, {Offset childFocalPoint}) { - final scaleState = scaleStateController.scaleState.state; + final scaleState = controller.scaleState.state; if (scaleState == ScaleState.zoomedIn || scaleState == ScaleState.zoomedOut) { - scaleStateController.setScaleState(scaleStateCycle(scaleState), source, childFocalPoint: childFocalPoint); + controller.setScaleState(scaleStateCycle(scaleState), source, childFocalPoint: childFocalPoint); return; } - final originalScale = getScaleForScaleState( - scaleState, - scaleBoundaries, - ); + final originalScale = controller.getScaleForScaleState(scaleState); var prevScale = originalScale; var prevScaleState = scaleState; @@ -125,11 +118,11 @@ mixin MagnifierControllerDelegate on State { prevScale = nextScale; prevScaleState = nextScaleState; nextScaleState = scaleStateCycle(prevScaleState); - nextScale = getScaleForScaleState(nextScaleState, scaleBoundaries); + nextScale = controller.getScaleForScaleState(nextScaleState); } while (prevScale == nextScale && scaleState != nextScaleState); if (originalScale == nextScale) return; - scaleStateController.setScaleState(nextScaleState, source, childFocalPoint: childFocalPoint); + controller.setScaleState(nextScaleState, source, childFocalPoint: childFocalPoint); } CornersRange cornersX({double scale}) { @@ -188,30 +181,10 @@ mixin MagnifierControllerDelegate on State { @override void dispose() { _animateScale = null; - _streamSubs.forEach((sub) => sub.cancel()); - _streamSubs.clear(); + _subscriptions.forEach((sub) => sub.cancel()); + _subscriptions.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 diff --git a/lib/widgets/common/magnifier/core/core.dart b/lib/widgets/common/magnifier/core/core.dart index fc6f20f68..916b10b49 100644 --- a/lib/widgets/common/magnifier/core/core.dart +++ b/lib/widgets/common/magnifier/core/core.dart @@ -5,7 +5,6 @@ import 'package:aves/widgets/common/magnifier/core/gesture_detector.dart'; import 'package:aves/widgets/common/magnifier/magnifier.dart'; import 'package:aves/widgets/common/magnifier/pan/corner_hit_detector.dart'; import 'package:aves/widgets/common/magnifier/scale/scale_boundaries.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'; @@ -18,17 +17,13 @@ class MagnifierCore extends StatefulWidget { @required this.onTap, @required this.gestureDetectorBehavior, @required this.controller, - @required this.scaleBoundaries, @required this.scaleStateCycle, - @required this.scaleStateController, @required this.applyScale, }) : super(key: key); final Widget child; final MagnifierController controller; - final MagnifierScaleStateController scaleStateController; - final ScaleBoundaries scaleBoundaries; final ScaleStateCycle scaleStateCycle; final MagnifierTapCallback onTap; @@ -59,7 +54,7 @@ class MagnifierCoreState extends State with TickerProviderStateMi } void handlePositionAnimate() { - controller.setPosition(_positionAnimation.value, ChangeSource.animation); + controller.update(position: _positionAnimation.value, source: ChangeSource.animation); } void onScaleStart(ScaleStartDetails details) { @@ -135,7 +130,7 @@ class MagnifierCoreState extends State with TickerProviderStateMi final viewportTapPosition = details.localPosition; final childTapPosition = scaleBoundaries.viewportToChildPosition(controller, viewportTapPosition); - widget.onTap.call(context, details, controller.value, childTapPosition); + widget.onTap.call(context, details, controller.currentState, childTapPosition); } void onDoubleTap(TapDownDetails details) { @@ -169,8 +164,8 @@ class MagnifierCoreState extends State with TickerProviderStateMi /// Check if scale is equal to initial after scale animation update void onAnimationStatusCompleted() { - if (scaleStateController.scaleState.state != ScaleState.initial && scale == scaleBoundaries.initialScale) { - scaleStateController.setScaleState(ScaleState.initial, ChangeSource.animation); + if (controller.scaleState.state != ScaleState.initial && scale == scaleBoundaries.initialScale) { + controller.setScaleState(ScaleState.initial, ChangeSource.animation); } } @@ -183,9 +178,9 @@ class MagnifierCoreState extends State with TickerProviderStateMi _positionAnimationController = AnimationController(vsync: this)..addListener(handlePositionAnimate); startListeners(); - addAnimateOnScaleStateUpdate(animateOnScaleStateUpdate); + setScaleStateUpdateAnimation(animateOnScaleStateUpdate); - cachedScaleBoundaries = widget.scaleBoundaries; + cachedScaleBoundaries = widget.controller.scaleBoundaries; } void animateOnScaleStateUpdate(double prevScale, double nextScale, Offset nextPosition) { @@ -204,49 +199,47 @@ class MagnifierCoreState extends State with TickerProviderStateMi @override Widget build(BuildContext context) { // Check if we need a recalc on the scale - if (widget.scaleBoundaries != cachedScaleBoundaries) { + if (widget.controller.scaleBoundaries != cachedScaleBoundaries) { markNeedsScaleRecalc = true; - cachedScaleBoundaries = widget.scaleBoundaries; + cachedScaleBoundaries = widget.controller.scaleBoundaries; } return StreamBuilder( - stream: controller.outputStateStream, - initialData: controller.prevValue, + stream: controller.stateStream, + initialData: controller.previousState, builder: (context, snapshot) { - if (snapshot.hasData) { - final value = snapshot.data; - final applyScale = widget.applyScale; + if (!snapshot.hasData) return Container(); - final computedScale = applyScale ? scale : 1.0; + final magnifierState = snapshot.data; + final position = magnifierState.position; + final applyScale = widget.applyScale; - final matrix = Matrix4.identity() - ..translate(value.position.dx, value.position.dy) - ..scale(computedScale); + Widget child = CustomSingleChildLayout( + delegate: _CenterWithOriginalSizeDelegate( + scaleBoundaries.childSize, + basePosition, + applyScale, + ), + child: widget.child, + ); - final Widget customChildLayout = CustomSingleChildLayout( - delegate: _CenterWithOriginalSizeDelegate( - scaleBoundaries.childSize, - basePosition, - applyScale, - ), - child: widget.child, - ); - return MagnifierGestureDetector( - child: Transform( - child: customChildLayout, - transform: matrix, - alignment: basePosition, - ), - onDoubleTap: onDoubleTap, - onScaleStart: onScaleStart, - onScaleUpdate: onScaleUpdate, - onScaleEnd: onScaleEnd, - hitDetector: this, - onTapUp: widget.onTap == null ? null : onTap, - ); - } else { - return Container(); - } + child = Transform( + transform: Matrix4.identity() + ..translate(position.dx, position.dy) + ..scale(applyScale ? scale : 1.0), + alignment: basePosition, + child: child, + ); + + return MagnifierGestureDetector( + child: child, + onDoubleTap: onDoubleTap, + onScaleStart: onScaleStart, + onScaleUpdate: onScaleUpdate, + onScaleEnd: onScaleEnd, + hitDetector: this, + onTapUp: widget.onTap == null ? null : onTap, + ); }); } } diff --git a/lib/widgets/common/magnifier/magnifier.dart b/lib/widgets/common/magnifier/magnifier.dart index 265b445f0..f5e18a5d3 100644 --- a/lib/widgets/common/magnifier/magnifier.dart +++ b/lib/widgets/common/magnifier/magnifier.dart @@ -3,7 +3,6 @@ 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/material.dart'; @@ -16,14 +15,13 @@ import 'package:flutter/material.dart'; /// - fixed corner hit detection when in containers scrollable in both axes /// - fixed corner hit detection issues due to imprecise double comparisons /// - added single & double tap position feedback -/// - fixed focusing on tap position when scaling by double tap +/// - fixed focus when scaling by double-tap/pinch class Magnifier extends StatefulWidget { const Magnifier({ Key key, @required this.child, this.childSize, this.controller, - this.scaleStateController, this.maxScale, this.minScale, this.initialScale, @@ -48,7 +46,6 @@ class Magnifier extends StatefulWidget { final ScaleLevel initialScale; final MagnifierController controller; - final MagnifierScaleStateController scaleStateController; final ScaleStateCycle scaleStateCycle; final MagnifierTapCallback onTap; final HitTestBehavior gestureDetectorBehavior; @@ -66,9 +63,6 @@ class _MagnifierState extends State { bool _controlledController; MagnifierController _controller; - bool _controlledScaleStateController; - MagnifierScaleStateController _scaleStateController; - void _setChildSize(Size childSize) { _childSize = childSize.isEmpty ? null : childSize; } @@ -84,14 +78,6 @@ class _MagnifierState extends State { _controlledController = false; _controller = widget.controller; } - - if (widget.scaleStateController == null) { - _controlledScaleStateController = true; - _scaleStateController = MagnifierScaleStateController(); - } else { - _controlledScaleStateController = false; - _scaleStateController = widget.scaleStateController; - } } @override @@ -110,16 +96,6 @@ class _MagnifierState extends State { _controlledController = false; _controller = widget.controller; } - - if (widget.scaleStateController == null) { - if (!_controlledScaleStateController) { - _controlledScaleStateController = true; - _scaleStateController = MagnifierScaleStateController(); - } - } else { - _controlledScaleStateController = false; - _scaleStateController = widget.scaleStateController; - } super.didUpdateWidget(oldWidget); } @@ -128,9 +104,6 @@ class _MagnifierState extends State { if (_controlledController) { _controller.dispose(); } - if (_controlledScaleStateController) { - _scaleStateController.dispose(); - } super.dispose(); } @@ -138,20 +111,18 @@ class _MagnifierState extends State { Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { - final scaleBoundaries = ScaleBoundaries( + _controller.setScaleBoundaries(ScaleBoundaries( widget.minScale ?? 0.0, widget.maxScale ?? ScaleLevel(factor: double.infinity), widget.initialScale ?? ScaleLevel(ref: ScaleReference.contained), constraints.biggest, _childSize ?? constraints.biggest, - ); + )); return MagnifierCore( child: widget.child, controller: _controller, - scaleStateController: _scaleStateController, scaleStateCycle: widget.scaleStateCycle ?? defaultScaleStateCycle, - scaleBoundaries: scaleBoundaries, onTap: widget.onTap, gestureDetectorBehavior: widget.gestureDetectorBehavior, applyScale: widget.applyScale ?? true, diff --git a/lib/widgets/common/magnifier/scale/scalestate_controller.dart b/lib/widgets/common/magnifier/scale/scalestate_controller.dart deleted file mode 100644 index 1296fe8de..000000000 --- a/lib/widgets/common/magnifier/scale/scalestate_controller.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'dart:async'; - -import 'package:aves/widgets/common/magnifier/controller/state.dart'; -import 'package:aves/widgets/common/magnifier/scale/state.dart'; -import 'package:flutter/rendering.dart'; - -typedef ScaleStateListener = void Function(double prevScale, double nextScale); - -class MagnifierScaleStateController { - ScaleStateChange _scaleState; - StreamController _outputScaleStateCtrl; - ScaleStateChange prevScaleState; - - Stream get scaleStateChangeStream => _outputScaleStateCtrl.stream; - - ScaleStateChange get scaleState => _scaleState; - - bool get hasChanged => prevScaleState != scaleState; - - bool get isZooming => scaleState.state == ScaleState.zoomedIn || scaleState.state == ScaleState.zoomedOut; - - MagnifierScaleStateController() { - _scaleState = ScaleStateChange(state: ScaleState.initial, source: ChangeSource.internal); - prevScaleState = _scaleState; - - _outputScaleStateCtrl = StreamController.broadcast(); - _outputScaleStateCtrl.sink.add(_scaleState); - } - - void dispose() { - _outputScaleStateCtrl.close(); - } - - void setScaleState(ScaleState newValue, ChangeSource source, {Offset childFocalPoint}) { - if (_scaleState.state == newValue) return; - - prevScaleState = _scaleState; - _scaleState = ScaleStateChange(state: newValue, source: source, childFocalPoint: childFocalPoint); - _outputScaleStateCtrl.sink.add(scaleState); - } -} diff --git a/lib/widgets/filter_grids/album_pick.dart b/lib/widgets/filter_grids/album_pick.dart index 2828a8266..c1ce9da15 100644 --- a/lib/widgets/filter_grids/album_pick.dart +++ b/lib/widgets/filter_grids/album_pick.dart @@ -177,9 +177,9 @@ class _AlbumFilterBarState extends State { ), ConstrainedBox( constraints: BoxConstraints(minWidth: 16), - child: AnimatedBuilder( - animation: _controller, - builder: (context, child) => AnimatedSwitcher( + child: ValueListenableBuilder( + valueListenable: _controller, + builder: (context, value, child) => AnimatedSwitcher( duration: Durations.appBarActionChangeAnimation, transitionBuilder: (child, animation) => FadeTransition( opacity: animation, @@ -189,7 +189,7 @@ class _AlbumFilterBarState extends State { child: child, ), ), - child: _controller.text.isNotEmpty ? clearButton : SizedBox.shrink(), + child: value.text.isNotEmpty ? clearButton : SizedBox.shrink(), ), ), ) diff --git a/lib/widgets/fullscreen/image_view.dart b/lib/widgets/fullscreen/image_view.dart index 4734d7c71..1c7040720 100644 --- a/lib/widgets/fullscreen/image_view.dart +++ b/lib/widgets/fullscreen/image_view.dart @@ -1,16 +1,19 @@ import 'dart:async'; +import 'dart:math'; import 'package:aves/image_providers/thumbnail_provider.dart'; import 'package:aves/image_providers/uri_picture_provider.dart'; import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/settings/entry_background.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/collection/empty.dart'; +import 'package:aves/widgets/common/fx/checkered_decoration.dart'; import 'package:aves/widgets/common/magnifier/controller/controller.dart'; import 'package:aves/widgets/common/magnifier/controller/state.dart'; import 'package:aves/widgets/common/magnifier/magnifier.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:aves/widgets/fullscreen/tiled_view.dart'; import 'package:aves/widgets/fullscreen/video_view.dart'; @@ -18,7 +21,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_ijkplayer/flutter_ijkplayer.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; class ImageView extends StatefulWidget { @@ -43,10 +45,8 @@ class ImageView extends StatefulWidget { class _ImageViewState extends State { final MagnifierController _magnifierController = MagnifierController(); - final MagnifierScaleStateController _magnifierScaleStateController = MagnifierScaleStateController(); final ValueNotifier _viewStateNotifier = ValueNotifier(ViewState.zero); - StreamSubscription _subscription; - Size _magnifierChildSize; + final List _subscriptions = []; static const initialScale = ScaleLevel(ref: ScaleReference.contained); static const minScale = ScaleLevel(ref: ScaleReference.contained); @@ -56,17 +56,20 @@ class _ImageViewState extends State { MagnifierTapCallback get onTap => widget.onTap; + static const decorationCheckSize = 20.0; + @override void initState() { super.initState(); - _subscription = _magnifierController.outputStateStream.listen(_onViewChanged); - _magnifierChildSize = entry.displaySize; + _subscriptions.add(_magnifierController.stateStream.listen(_onViewStateChanged)); + _subscriptions.add(_magnifierController.scaleBoundariesStream.listen(_onViewScaleBoundariesChanged)); } @override void dispose() { - _subscription.cancel(); - _subscription = null; + _subscriptions + ..forEach((sub) => sub.cancel()) + ..clear(); widget.onDisposed?.call(); super.dispose(); } @@ -118,30 +121,14 @@ class _ImageViewState extends State { return Magnifier( // key includes size and orientation to refresh when the image is rotated key: ValueKey('${entry.rotationDegrees}_${entry.isFlipped}_${entry.width}_${entry.height}_${entry.path}'), - child: Selector( - selector: (context, mq) => mq.size, - builder: (context, mqSize, child) { - // When the scale state is cycled to be in its `initial` state (i.e. `contained`), and the device is rotated, - // `Magnifier` keeps the scale state as `contained`, but the controller does not update or notify the new scale value. - // We cannot monitor scale state changes as a workaround, because the scale state is updated before animating the scale, - // so we keep receiving scale updates after the scale state update. - // Instead we check the scale state here when the constraints change, so we can reset the obsolete scale value. - if (_magnifierScaleStateController.scaleState.state == ScaleState.initial) { - final value = MagnifierState(position: Offset.zero, scale: 0, source: ChangeSource.internal); - WidgetsBinding.instance.addPostFrameCallback((_) => _onViewChanged(value)); - } - return TiledImageView( - entry: entry, - viewportSize: mqSize, - viewStateNotifier: _viewStateNotifier, - baseChild: _loadingBuilder(context, fastThumbnailProvider), - errorBuilder: (context, error, stackTrace) => ErrorChild(onTap: () => onTap?.call(null)), - ); - }, + child: TiledImageView( + entry: entry, + viewStateNotifier: _viewStateNotifier, + baseChild: _loadingBuilder(context, fastThumbnailProvider), + errorBuilder: (context, error, stackTrace) => ErrorChild(onTap: () => onTap?.call(null)), ), childSize: entry.displaySize, controller: _magnifierController, - scaleStateController: _magnifierScaleStateController, maxScale: maxScale, minScale: minScale, initialScale: initialScale, @@ -151,8 +138,10 @@ class _ImageViewState extends State { } Widget _buildSvgView() { - final colorFilter = ColorFilter.mode(Color(settings.svgBackground), BlendMode.dstOver); - return Magnifier( + final background = settings.vectorBackground; + final colorFilter = background.isColor ? ColorFilter.mode(background.color, BlendMode.dstOver) : null; + + Widget child = Magnifier( child: SvgPicture( UriPicture( uri: entry.uri, @@ -167,6 +156,42 @@ class _ImageViewState extends State { scaleStateCycle: _vectorScaleStateCycle, onTap: (c, d, s, childPosition) => onTap?.call(childPosition), ); + + if (background == EntryBackground.checkered) { + child = ValueListenableBuilder( + valueListenable: _viewStateNotifier, + builder: (context, viewState, child) { + final viewportSize = viewState.viewportSize; + if (viewportSize == null) return child; + + final side = viewportSize.shortestSide; + final checkSize = side / ((side / decorationCheckSize).round()); + + final viewSize = entry.displaySize * viewState.scale; + final decorationSize = Size(min(viewSize.width, viewportSize.width), min(viewSize.height, viewportSize.height)); + final offset = Offset(decorationSize.width - viewportSize.width, decorationSize.height - viewportSize.height) / 2; + + return Stack( + alignment: Alignment.center, + children: [ + Positioned( + width: decorationSize.width, + height: decorationSize.height, + child: DecoratedBox( + decoration: CheckeredDecoration( + checkSize: checkSize, + offset: offset, + ), + ), + ), + child, + ], + ); + }, + child: child, + ); + } + return child; } Widget _buildVideoView() { @@ -187,8 +212,16 @@ class _ImageViewState extends State { ); } - void _onViewChanged(MagnifierState v) { - final viewState = ViewState(v.position, v.scale, _magnifierChildSize); + void _onViewStateChanged(MagnifierState v) { + final current = _viewStateNotifier.value; + final viewState = ViewState(v.position, v.scale, current.viewportSize); + _viewStateNotifier.value = viewState; + ViewStateNotification(entry.uri, viewState).dispatch(context); + } + + void _onViewScaleBoundariesChanged(ScaleBoundaries v) { + final current = _viewStateNotifier.value; + final viewState = ViewState(current.position, current.scale, v.viewportSize); _viewStateNotifier.value = viewState; ViewStateNotification(entry.uri, viewState).dispatch(context); } @@ -206,14 +239,14 @@ class _ImageViewState extends State { class ViewState { final Offset position; final double scale; - final Size size; + final Size viewportSize; - static const ViewState zero = ViewState(Offset(0.0, 0.0), 0, null); + static const ViewState zero = ViewState(Offset.zero, 0, null); - const ViewState(this.position, this.scale, this.size); + const ViewState(this.position, this.scale, this.viewportSize); @override - String toString() => '$runtimeType#${shortHash(this)}{position=$position, scale=$scale, size=$size}'; + String toString() => '$runtimeType#${shortHash(this)}{position=$position, scale=$scale, viewportSize=$viewportSize}'; } class ViewStateNotification extends Notification { diff --git a/lib/widgets/fullscreen/info/metadata/metadata_section.dart b/lib/widgets/fullscreen/info/metadata/metadata_section.dart index 852ea2897..e12c8822d 100644 --- a/lib/widgets/fullscreen/info/metadata/metadata_section.dart +++ b/lib/widgets/fullscreen/info/metadata/metadata_section.dart @@ -92,9 +92,9 @@ class _MetadataSectionSliverState extends State with Auto // cancel notification bubbling so that the info page // does not misinterpret content scrolling for page scrolling onNotification: (notification) => true, - child: AnimatedBuilder( - animation: _loadedMetadataUri, - builder: (context, child) { + child: ValueListenableBuilder( + valueListenable: _loadedMetadataUri, + builder: (context, uri, child) { Widget content; if (_metadata.isEmpty) { content = SizedBox.shrink(); @@ -119,7 +119,7 @@ class _MetadataSectionSliverState extends State with Auto return AnimationLimiter( // we update the limiter key after fetching the metadata of a new entry, // in order to restart the staggered animation of the metadata section - key: Key(_loadedMetadataUri.value), + key: Key(uri), child: content, ); }, diff --git a/lib/widgets/fullscreen/overlay/minimap.dart b/lib/widgets/fullscreen/overlay/minimap.dart index 94c6edad1..561cc3971 100644 --- a/lib/widgets/fullscreen/overlay/minimap.dart +++ b/lib/widgets/fullscreen/overlay/minimap.dart @@ -4,7 +4,6 @@ import 'package:aves/model/image_entry.dart'; import 'package:aves/widgets/fullscreen/image_view.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; class Minimap extends StatelessWidget { final ImageEntry entry; @@ -22,24 +21,21 @@ class Minimap extends StatelessWidget { @override Widget build(BuildContext context) { return IgnorePointer( - child: Selector( - selector: (context, mq) => mq.size, - builder: (context, mqSize, child) { - return AnimatedBuilder( - animation: viewStateNotifier, - builder: (context, child) { - final viewState = viewStateNotifier.value; - return CustomPaint( - painter: MinimapPainter( - viewportSize: mqSize, - entrySize: viewState.size ?? entry.displaySize, - viewCenterOffset: viewState.position, - viewScale: viewState.scale, - minimapBorderColor: Colors.white30, - ), - size: size, - ); - }); + child: ValueListenableBuilder( + valueListenable: viewStateNotifier, + builder: (context, viewState, child) { + final viewportSize = viewState.viewportSize; + if (viewportSize == null) return SizedBox.shrink(); + return CustomPaint( + painter: MinimapPainter( + viewportSize: viewportSize, + entrySize: entry.displaySize, + viewCenterOffset: viewState.position, + viewScale: viewState.scale, + minimapBorderColor: Colors.white30, + ), + size: size, + ); }), ); } diff --git a/lib/widgets/fullscreen/tiled_view.dart b/lib/widgets/fullscreen/tiled_view.dart index 758db440b..cf829c479 100644 --- a/lib/widgets/fullscreen/tiled_view.dart +++ b/lib/widgets/fullscreen/tiled_view.dart @@ -10,14 +10,12 @@ import 'package:flutter/material.dart'; class TiledImageView extends StatefulWidget { final ImageEntry entry; - final Size viewportSize; final ValueNotifier viewStateNotifier; final Widget baseChild; final ImageErrorWidgetBuilder errorBuilder; const TiledImageView({ @required this.entry, - @required this.viewportSize, @required this.viewStateNotifier, @required this.baseChild, @required this.errorBuilder, @@ -28,14 +26,13 @@ class TiledImageView extends StatefulWidget { } class _TiledImageViewState extends State { + bool _initialized = false; double _tileSide, _initialScale; int _maxSampleSize; Matrix4 _transform; ImageEntry get entry => widget.entry; - Size get viewportSize => widget.viewportSize; - ValueNotifier get viewStateNotifier => widget.viewStateNotifier; bool get useTiles => entry.canTile && (entry.width > 4096 || entry.height > 4096); @@ -51,24 +48,21 @@ class _TiledImageViewState extends State { // magic number used to derive sample size from scale static const scaleFactor = 2.0; - @override - void initState() { - super.initState(); - _init(); - } - @override void didUpdateWidget(TiledImageView oldWidget) { super.didUpdateWidget(oldWidget); - if (oldWidget.viewportSize != widget.viewportSize || oldWidget.entry.displaySize != widget.entry.displaySize) { - _init(); + final oldViewState = oldWidget.viewStateNotifier.value; + final viewState = widget.viewStateNotifier.value; + if (oldViewState.viewportSize != viewState.viewportSize || oldWidget.entry.displaySize != widget.entry.displaySize) { + _initialized = false; } } - void _init() { + void _initFromViewport(Size viewportSize) { + final displaySize = entry.displaySize; _tileSide = viewportSize.shortestSide * scaleFactor; - _initialScale = min(viewportSize.width / entry.displaySize.width, viewportSize.height / entry.displaySize.height); + _initialScale = min(viewportSize.width / displaySize.width, viewportSize.height / displaySize.height); _maxSampleSize = _sampleSizeForScale(_initialScale); final rotationDegrees = entry.rotationDegrees; @@ -79,8 +73,9 @@ class _TiledImageViewState extends State { ..translate(entry.width / 2.0, entry.height / 2.0) ..scale(isFlipped ? -1.0 : 1.0, 1.0, 1.0) ..rotateZ(-toRadians(rotationDegrees.toDouble())) - ..translate(-entry.displaySize.width / 2.0, -entry.displaySize.height / 2.0); + ..translate(-displaySize.width / 2.0, -displaySize.height / 2.0); } + _initialized = true; } @override @@ -90,10 +85,13 @@ class _TiledImageViewState extends State { final displayWidth = entry.displaySize.width.round(); final displayHeight = entry.displaySize.height.round(); - return AnimatedBuilder( - animation: viewStateNotifier, - builder: (context, child) { - final viewState = viewStateNotifier.value; + return ValueListenableBuilder( + valueListenable: viewStateNotifier, + builder: (context, viewState, child) { + final viewportSize = viewState.viewportSize; + if (viewportSize == null) return SizedBox.shrink(); + if (!_initialized) _initFromViewport(viewportSize); + var scale = viewState.scale; if (scale == 0.0) { // for initial scale as `contained` @@ -136,6 +134,7 @@ class _TiledImageViewState extends State { List _getTiles(ViewState viewState, int displayWidth, int displayHeight, double scale) { final centerOffset = viewState.position; + final viewportSize = viewState.viewportSize; final viewOrigin = Offset( ((displayWidth * scale - viewportSize.width) / 2 - centerOffset.dx), ((displayHeight * scale - viewportSize.height) / 2 - centerOffset.dy), diff --git a/lib/widgets/settings/svg_background.dart b/lib/widgets/settings/svg_background.dart index 929b5a8ad..a2c68e251 100644 --- a/lib/widgets/settings/svg_background.dart +++ b/lib/widgets/settings/svg_background.dart @@ -1,5 +1,7 @@ +import 'package:aves/model/settings/entry_background.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/widgets/common/fx/borders.dart'; +import 'package:aves/widgets/common/fx/checkered_decoration.dart'; import 'package:flutter/material.dart'; class SvgBackgroundSelector extends StatefulWidget { @@ -10,33 +12,53 @@ class SvgBackgroundSelector extends StatefulWidget { class _SvgBackgroundSelectorState extends State { @override Widget build(BuildContext context) { - const radius = 24.0; + const radius = 12.0; return DropdownButtonHideUnderline( - child: DropdownButton( - items: [0xFFFFFFFF, 0xFF000000, 0x00000000].map((selected) { - return DropdownMenuItem( + child: DropdownButton( + items: [ + EntryBackground.white, + EntryBackground.black, + EntryBackground.checkered, + EntryBackground.transparent, + ].map((selected) { + Widget child; + switch (selected) { + case EntryBackground.transparent: + child = Icon( + Icons.clear, + size: 20, + color: Colors.white30, + ); + break; + case EntryBackground.checkered: + child = ClipOval( + child: DecoratedBox( + decoration: CheckeredDecoration( + checkSize: radius, + ), + ), + ); + break; + default: + break; + } + return DropdownMenuItem( value: selected, child: Container( - height: radius, - width: radius, + height: radius * 2, + width: radius * 2, decoration: BoxDecoration( - color: Color(selected), + color: selected.isColor ? selected.color : null, border: AvesCircleBorder.build(context), shape: BoxShape.circle, ), - child: selected == 0 - ? Icon( - Icons.clear, - size: 20, - color: Colors.white30, - ) - : null, + child: child, ), ); }).toList(), - value: settings.svgBackground, + value: settings.vectorBackground, onChanged: (selected) { - settings.svgBackground = selected; + settings.vectorBackground = selected; setState(() {}); }, ),