svg: optional checkered background

This commit is contained in:
Thibault Deckers 2020-12-21 20:11:14 +09:00
parent c9fb94f326
commit b14558e451
17 changed files with 443 additions and 368 deletions

View file

@ -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;
}
}
}

View file

@ -1,5 +1,6 @@
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/settings/coordinate_format.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/home_page.dart';
import 'package:aves/model/settings/screen_on.dart'; import 'package:aves/model/settings/screen_on.dart';
import 'package:aves/widgets/fullscreen/info/location_section.dart'; import 'package:aves/widgets/fullscreen/info/location_section.dart';
@ -54,7 +55,7 @@ class Settings extends ChangeNotifier {
static const coordinateFormatKey = 'coordinates_format'; static const coordinateFormatKey = 'coordinates_format';
// rendering // rendering
static const svgBackgroundKey = 'svg_background'; static const vectorBackgroundKey = 'vector_background';
// search // search
static const saveSearchHistoryKey = 'save_search_history'; static const saveSearchHistoryKey = 'save_search_history';
@ -184,9 +185,9 @@ class Settings extends ChangeNotifier {
// rendering // 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 // search

View file

@ -43,7 +43,6 @@ class DecoratedThumbnail extends StatelessWidget {
); );
child = Stack( child = Stack(
fit: StackFit.passthrough,
children: [ children: [
child, child,
Positioned( Positioned(

View file

@ -1,6 +1,10 @@
import 'package:aves/model/image_entry.dart'; import 'dart:math';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/image_providers/uri_picture_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/widgets/common/fx/checkered_decoration.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -19,23 +23,39 @@ class ThumbnailVectorImage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final child = Container( final pictureProvider = UriPicture(
// center `SvgPicture` inside `Container` with the thumbnail dimensions uri: entry.uri,
// so that `SvgPicture` doesn't get aligned by the `Stack` like the overlay icons mimeType: entry.mimeType,
width: extent, );
height: extent,
child: Selector<Settings, int>( final child = Center(
selector: (context, s) => s.svgBackground, child: Selector<Settings, EntryBackground>(
builder: (context, svgBackground, child) { selector: (context, s) => s.vectorBackground,
final colorFilter = ColorFilter.mode(Color(svgBackground), BlendMode.dstOver); builder: (context, background, child) {
return SvgPicture( if (background == EntryBackground.transparent) {
UriPicture( return SvgPicture(
uri: entry.uri, pictureProvider,
mimeType: entry.mimeType, width: extent,
colorFilter: colorFilter, height: extent,
), );
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,
); );
}, },
), ),

View file

@ -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();
}
}

View file

@ -6,15 +6,15 @@ class HighlightDecoration extends Decoration {
const HighlightDecoration({@required this.color}); const HighlightDecoration({@required this.color});
@override @override
HighlightDecorationPainter createBoxPainter([VoidCallback onChanged]) { _HighlightDecorationPainter createBoxPainter([VoidCallback onChanged]) {
return HighlightDecorationPainter(this, onChanged); return _HighlightDecorationPainter(this, onChanged);
} }
} }
class HighlightDecorationPainter extends BoxPainter { class _HighlightDecorationPainter extends BoxPainter {
final HighlightDecoration decoration; final HighlightDecoration decoration;
const HighlightDecorationPainter(this.decoration, VoidCallback onChanged) : super(onChanged); const _HighlightDecorationPainter(this.decoration, VoidCallback onChanged) : super(onChanged);
@override @override
void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) { void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {

View file

@ -2,101 +2,127 @@ import 'dart:async';
import 'dart:ui'; import 'dart:ui';
import 'package:aves/widgets/common/magnifier/controller/state.dart'; 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'; import 'package:flutter/widgets.dart';
class MagnifierController { class MagnifierController {
final StreamController<MagnifierState> _stateStreamController = StreamController.broadcast();
final StreamController<ScaleBoundaries> _scaleBoundariesStreamController = StreamController.broadcast();
final StreamController<ScaleStateChange> _scaleStateChangeStreamController = StreamController.broadcast();
MagnifierState _currentState, initial, previousState;
ScaleBoundaries _scaleBoundaries;
ScaleStateChange _currentScaleState, previousScaleState;
MagnifierController({ MagnifierController({
Offset initialPosition = Offset.zero, Offset initialPosition = Offset.zero,
}) : _valueNotifier = ValueNotifier( }) : super() {
MagnifierState( initial = MagnifierState(
position: initialPosition, position: initialPosition,
scale: null, scale: null,
source: ChangeSource.internal, source: ChangeSource.internal,
), );
), previousState = initial;
super() { _setState(initial);
initial = value;
prevValue = initial;
_valueNotifier.addListener(_changeListener); final _initialScaleState = ScaleStateChange(state: ScaleState.initial, source: ChangeSource.internal);
_outputCtrl = StreamController<MagnifierState>.broadcast(); previousScaleState = _initialScaleState;
_outputCtrl.sink.add(initial); _setScaleState(_initialScaleState);
} }
final ValueNotifier<MagnifierState> _valueNotifier; Stream<MagnifierState> get stateStream => _stateStreamController.stream;
MagnifierState initial; Stream<ScaleBoundaries> get scaleBoundariesStream => _scaleBoundariesStreamController.stream;
StreamController<MagnifierState> _outputCtrl; Stream<ScaleStateChange> get scaleStateChangeStream => _scaleStateChangeStreamController.stream;
/// The output for state/value updates. Usually a broadcast [Stream] MagnifierState get currentState => _currentState;
Stream<MagnifierState> get outputStateStream => _outputCtrl.stream;
/// The state value before the last change or the initial state if the state has not been changed. Offset get position => currentState.position;
MagnifierState prevValue;
/// Resets the state to the initial value; double get scale => currentState.scale;
void reset() {
_setValue(initial);
}
void _changeListener() { ScaleBoundaries get scaleBoundaries => _scaleBoundaries;
_outputCtrl.sink.add(value);
} 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. /// Closes streams and removes eventual listeners.
void dispose() { void dispose() {
_outputCtrl.close(); _stateStreamController.close();
_valueNotifier.dispose(); _scaleBoundariesStreamController.close();
_scaleStateChangeStreamController.close();
} }
void setPosition(Offset position, ChangeSource source) { void update({
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({
Offset position, Offset position,
double scale, double scale,
@required ChangeSource source, @required ChangeSource source,
}) { }) {
prevValue = value; position = position ?? this.position;
_setValue(MagnifierState( scale = scale ?? this.scale;
position: position ?? value.position, if (this.position == position && this.scale == scale) return;
scale: scale ?? value.scale,
previousState = currentState;
_setState(MagnifierState(
position: position,
scale: scale,
source: source, source: source,
)); ));
} }
/// The actual state value void setScaleState(ScaleState newValue, ChangeSource source, {Offset childFocalPoint}) {
MagnifierState get value => _valueNotifier.value; if (_currentScaleState.state == newValue) return;
void _setValue(MagnifierState newValue) { previousScaleState = _currentScaleState;
if (_valueNotifier.value == newValue) return; _currentScaleState = ScaleStateChange(state: newValue, source: source, childFocalPoint: childFocalPoint);
_valueNotifier.value = newValue; _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;
}
} }
} }

View file

@ -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/controller/state.dart';
import 'package:aves/widgets/common/magnifier/core/core.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_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/common/magnifier/scale/state.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
@ -16,9 +14,7 @@ import 'package:flutter/widgets.dart';
mixin MagnifierControllerDelegate on State<MagnifierCore> { mixin MagnifierControllerDelegate on State<MagnifierCore> {
MagnifierController get controller => widget.controller; MagnifierController get controller => widget.controller;
MagnifierScaleStateController get scaleStateController => widget.scaleStateController; ScaleBoundaries get scaleBoundaries => controller.scaleBoundaries;
ScaleBoundaries get scaleBoundaries => widget.scaleBoundaries;
ScaleStateCycle get scaleStateCycle => widget.scaleStateCycle; ScaleStateCycle get scaleStateCycle => widget.scaleStateCycle;
@ -29,24 +25,24 @@ mixin MagnifierControllerDelegate on State<MagnifierCore> {
/// Mark if scale need recalculation, useful for scale boundaries changes. /// Mark if scale need recalculation, useful for scale boundaries changes.
bool markNeedsScaleRecalc = true; bool markNeedsScaleRecalc = true;
final List<StreamSubscription> _streamSubs = []; final List<StreamSubscription> _subscriptions = [];
void startListeners() { void startListeners() {
_streamSubs.add(controller.outputStateStream.listen(_onMagnifierStateChange)); _subscriptions.add(controller.stateStream.listen(_onMagnifierStateChange));
_streamSubs.add(scaleStateController.scaleStateChangeStream.listen(_onScaleStateChange)); _subscriptions.add(controller.scaleStateChangeStream.listen(_onScaleStateChange));
} }
void _onScaleStateChange(ScaleStateChange scaleStateChange) { void _onScaleStateChange(ScaleStateChange scaleStateChange) {
if (scaleStateChange.source == ChangeSource.internal) return; if (scaleStateChange.source == ChangeSource.internal) return;
if (!scaleStateController.hasChanged) return; if (!controller.hasScaleSateChanged) return;
if (_animateScale == null || scaleStateController.isZooming) { if (_animateScale == null || controller.isZooming) {
controller.setScale(scale, scaleStateChange.source); controller.update(scale: scale, source: scaleStateChange.source);
return; return;
} }
final nextScaleState = scaleStateChange.state; final nextScaleState = scaleStateChange.state;
final nextScale = getScaleForScaleState(nextScaleState, scaleBoundaries); final nextScale = controller.getScaleForScaleState(nextScaleState);
var nextPosition = Offset.zero; var nextPosition = Offset.zero;
if (nextScaleState == ScaleState.covering || nextScaleState == ScaleState.originalSize) { if (nextScaleState == ScaleState.covering || nextScaleState == ScaleState.originalSize) {
final childFocalPoint = scaleStateChange.childFocalPoint; final childFocalPoint = scaleStateChange.childFocalPoint;
@ -55,31 +51,31 @@ mixin MagnifierControllerDelegate on State<MagnifierCore> {
} }
} }
final prevScale = controller.scale ?? getScaleForScaleState(scaleStateController.prevScaleState.state, scaleBoundaries); final prevScale = controller.scale ?? controller.getScaleForScaleState(controller.previousScaleState.state);
_animateScale(prevScale, nextScale, nextPosition); _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; _animateScale = animateScale;
} }
void _onMagnifierStateChange(MagnifierState state) { void _onMagnifierStateChange(MagnifierState state) {
controller.setPosition(clampPosition(), state.source); controller.update(position: clampPosition(), source: state.source);
if (controller.scale == controller.prevValue.scale) return; if (controller.scale == controller.previousState.scale) return;
if (state.source == ChangeSource.internal || state.source == ChangeSource.animation) return; if (state.source == ChangeSource.internal || state.source == ChangeSource.animation) return;
final newScaleState = (scale > scaleBoundaries.initialScale) ? ScaleState.zoomedIn : ScaleState.zoomedOut; final newScaleState = (scale > scaleBoundaries.initialScale) ? ScaleState.zoomedIn : ScaleState.zoomedOut;
scaleStateController.setScaleState(newScaleState, state.source); controller.setScaleState(newScaleState, state.source);
} }
Offset get position => controller.position; Offset get position => controller.position;
double get scale { double get scale {
final scaleState = scaleStateController.scaleState.state; final scaleState = controller.scaleState.state;
final needsRecalc = markNeedsScaleRecalc && !(scaleState == ScaleState.zoomedIn || scaleState == ScaleState.zoomedOut); final needsRecalc = markNeedsScaleRecalc && !(scaleState == ScaleState.zoomedIn || scaleState == ScaleState.zoomedOut);
final scaleExistsOnController = controller.scale != null; final scaleExistsOnController = controller.scale != null;
if (needsRecalc || !scaleExistsOnController) { if (needsRecalc || !scaleExistsOnController) {
final newScale = getScaleForScaleState(scaleState, scaleBoundaries); final newScale = controller.getScaleForScaleState(scaleState);
markNeedsScaleRecalc = false; markNeedsScaleRecalc = false;
setScale(newScale, ChangeSource.internal); setScale(newScale, ChangeSource.internal);
return newScale; return newScale;
@ -87,14 +83,14 @@ mixin MagnifierControllerDelegate on State<MagnifierCore> {
return controller.scale; 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({ void updateMultiple({
Offset position, @required Offset position,
double scale, @required double scale,
@required ChangeSource source, @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) { void updateScaleStateFromNewScale(double newScale, ChangeSource source) {
@ -102,19 +98,16 @@ mixin MagnifierControllerDelegate on State<MagnifierCore> {
if (scale != scaleBoundaries.initialScale) { if (scale != scaleBoundaries.initialScale) {
newScaleState = (newScale > scaleBoundaries.initialScale) ? ScaleState.zoomedIn : ScaleState.zoomedOut; newScaleState = (newScale > scaleBoundaries.initialScale) ? ScaleState.zoomedIn : ScaleState.zoomedOut;
} }
scaleStateController.setScaleState(newScaleState, source); controller.setScaleState(newScaleState, source);
} }
void nextScaleState(ChangeSource source, {Offset childFocalPoint}) { void nextScaleState(ChangeSource source, {Offset childFocalPoint}) {
final scaleState = scaleStateController.scaleState.state; final scaleState = controller.scaleState.state;
if (scaleState == ScaleState.zoomedIn || scaleState == ScaleState.zoomedOut) { if (scaleState == ScaleState.zoomedIn || scaleState == ScaleState.zoomedOut) {
scaleStateController.setScaleState(scaleStateCycle(scaleState), source, childFocalPoint: childFocalPoint); controller.setScaleState(scaleStateCycle(scaleState), source, childFocalPoint: childFocalPoint);
return; return;
} }
final originalScale = getScaleForScaleState( final originalScale = controller.getScaleForScaleState(scaleState);
scaleState,
scaleBoundaries,
);
var prevScale = originalScale; var prevScale = originalScale;
var prevScaleState = scaleState; var prevScaleState = scaleState;
@ -125,11 +118,11 @@ mixin MagnifierControllerDelegate on State<MagnifierCore> {
prevScale = nextScale; prevScale = nextScale;
prevScaleState = nextScaleState; prevScaleState = nextScaleState;
nextScaleState = scaleStateCycle(prevScaleState); nextScaleState = scaleStateCycle(prevScaleState);
nextScale = getScaleForScaleState(nextScaleState, scaleBoundaries); nextScale = controller.getScaleForScaleState(nextScaleState);
} while (prevScale == nextScale && scaleState != nextScaleState); } while (prevScale == nextScale && scaleState != nextScaleState);
if (originalScale == nextScale) return; if (originalScale == nextScale) return;
scaleStateController.setScaleState(nextScaleState, source, childFocalPoint: childFocalPoint); controller.setScaleState(nextScaleState, source, childFocalPoint: childFocalPoint);
} }
CornersRange cornersX({double scale}) { CornersRange cornersX({double scale}) {
@ -188,30 +181,10 @@ mixin MagnifierControllerDelegate on State<MagnifierCore> {
@override @override
void dispose() { void dispose() {
_animateScale = null; _animateScale = null;
_streamSubs.forEach((sub) => sub.cancel()); _subscriptions.forEach((sub) => sub.cancel());
_streamSubs.clear(); _subscriptions.clear();
super.dispose(); 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 /// Simple class to store a min and a max value

View file

@ -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/magnifier.dart';
import 'package:aves/widgets/common/magnifier/pan/corner_hit_detector.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/scale_boundaries.dart';
import 'package:aves/widgets/common/magnifier/scale/scalestate_controller.dart';
import 'package:aves/widgets/common/magnifier/scale/state.dart'; import 'package:aves/widgets/common/magnifier/scale/state.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
@ -18,17 +17,13 @@ class MagnifierCore extends StatefulWidget {
@required this.onTap, @required this.onTap,
@required this.gestureDetectorBehavior, @required this.gestureDetectorBehavior,
@required this.controller, @required this.controller,
@required this.scaleBoundaries,
@required this.scaleStateCycle, @required this.scaleStateCycle,
@required this.scaleStateController,
@required this.applyScale, @required this.applyScale,
}) : super(key: key); }) : super(key: key);
final Widget child; final Widget child;
final MagnifierController controller; final MagnifierController controller;
final MagnifierScaleStateController scaleStateController;
final ScaleBoundaries scaleBoundaries;
final ScaleStateCycle scaleStateCycle; final ScaleStateCycle scaleStateCycle;
final MagnifierTapCallback onTap; final MagnifierTapCallback onTap;
@ -59,7 +54,7 @@ class MagnifierCoreState extends State<MagnifierCore> with TickerProviderStateMi
} }
void handlePositionAnimate() { void handlePositionAnimate() {
controller.setPosition(_positionAnimation.value, ChangeSource.animation); controller.update(position: _positionAnimation.value, source: ChangeSource.animation);
} }
void onScaleStart(ScaleStartDetails details) { void onScaleStart(ScaleStartDetails details) {
@ -135,7 +130,7 @@ class MagnifierCoreState extends State<MagnifierCore> with TickerProviderStateMi
final viewportTapPosition = details.localPosition; final viewportTapPosition = details.localPosition;
final childTapPosition = scaleBoundaries.viewportToChildPosition(controller, viewportTapPosition); 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) { void onDoubleTap(TapDownDetails details) {
@ -169,8 +164,8 @@ class MagnifierCoreState extends State<MagnifierCore> with TickerProviderStateMi
/// Check if scale is equal to initial after scale animation update /// Check if scale is equal to initial after scale animation update
void onAnimationStatusCompleted() { void onAnimationStatusCompleted() {
if (scaleStateController.scaleState.state != ScaleState.initial && scale == scaleBoundaries.initialScale) { if (controller.scaleState.state != ScaleState.initial && scale == scaleBoundaries.initialScale) {
scaleStateController.setScaleState(ScaleState.initial, ChangeSource.animation); controller.setScaleState(ScaleState.initial, ChangeSource.animation);
} }
} }
@ -183,9 +178,9 @@ class MagnifierCoreState extends State<MagnifierCore> with TickerProviderStateMi
_positionAnimationController = AnimationController(vsync: this)..addListener(handlePositionAnimate); _positionAnimationController = AnimationController(vsync: this)..addListener(handlePositionAnimate);
startListeners(); startListeners();
addAnimateOnScaleStateUpdate(animateOnScaleStateUpdate); setScaleStateUpdateAnimation(animateOnScaleStateUpdate);
cachedScaleBoundaries = widget.scaleBoundaries; cachedScaleBoundaries = widget.controller.scaleBoundaries;
} }
void animateOnScaleStateUpdate(double prevScale, double nextScale, Offset nextPosition) { void animateOnScaleStateUpdate(double prevScale, double nextScale, Offset nextPosition) {
@ -204,49 +199,47 @@ class MagnifierCoreState extends State<MagnifierCore> with TickerProviderStateMi
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Check if we need a recalc on the scale // Check if we need a recalc on the scale
if (widget.scaleBoundaries != cachedScaleBoundaries) { if (widget.controller.scaleBoundaries != cachedScaleBoundaries) {
markNeedsScaleRecalc = true; markNeedsScaleRecalc = true;
cachedScaleBoundaries = widget.scaleBoundaries; cachedScaleBoundaries = widget.controller.scaleBoundaries;
} }
return StreamBuilder<MagnifierState>( return StreamBuilder<MagnifierState>(
stream: controller.outputStateStream, stream: controller.stateStream,
initialData: controller.prevValue, initialData: controller.previousState,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.hasData) { if (!snapshot.hasData) return Container();
final value = snapshot.data;
final applyScale = widget.applyScale;
final computedScale = applyScale ? scale : 1.0; final magnifierState = snapshot.data;
final position = magnifierState.position;
final applyScale = widget.applyScale;
final matrix = Matrix4.identity() Widget child = CustomSingleChildLayout(
..translate(value.position.dx, value.position.dy) delegate: _CenterWithOriginalSizeDelegate(
..scale(computedScale); scaleBoundaries.childSize,
basePosition,
applyScale,
),
child: widget.child,
);
final Widget customChildLayout = CustomSingleChildLayout( child = Transform(
delegate: _CenterWithOriginalSizeDelegate( transform: Matrix4.identity()
scaleBoundaries.childSize, ..translate(position.dx, position.dy)
basePosition, ..scale(applyScale ? scale : 1.0),
applyScale, alignment: basePosition,
), child: child,
child: widget.child, );
);
return MagnifierGestureDetector( return MagnifierGestureDetector(
child: Transform( child: child,
child: customChildLayout, onDoubleTap: onDoubleTap,
transform: matrix, onScaleStart: onScaleStart,
alignment: basePosition, onScaleUpdate: onScaleUpdate,
), onScaleEnd: onScaleEnd,
onDoubleTap: onDoubleTap, hitDetector: this,
onScaleStart: onScaleStart, onTapUp: widget.onTap == null ? null : onTap,
onScaleUpdate: onScaleUpdate, );
onScaleEnd: onScaleEnd,
hitDetector: this,
onTapUp: widget.onTap == null ? null : onTap,
);
} else {
return Container();
}
}); });
} }
} }

View file

@ -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/core/core.dart';
import 'package:aves/widgets/common/magnifier/scale/scale_boundaries.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/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/common/magnifier/scale/state.dart';
import 'package:flutter/material.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 when in containers scrollable in both axes
/// - fixed corner hit detection issues due to imprecise double comparisons /// - fixed corner hit detection issues due to imprecise double comparisons
/// - added single & double tap position feedback /// - 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 { class Magnifier extends StatefulWidget {
const Magnifier({ const Magnifier({
Key key, Key key,
@required this.child, @required this.child,
this.childSize, this.childSize,
this.controller, this.controller,
this.scaleStateController,
this.maxScale, this.maxScale,
this.minScale, this.minScale,
this.initialScale, this.initialScale,
@ -48,7 +46,6 @@ class Magnifier extends StatefulWidget {
final ScaleLevel initialScale; final ScaleLevel initialScale;
final MagnifierController controller; final MagnifierController controller;
final MagnifierScaleStateController scaleStateController;
final ScaleStateCycle scaleStateCycle; final ScaleStateCycle scaleStateCycle;
final MagnifierTapCallback onTap; final MagnifierTapCallback onTap;
final HitTestBehavior gestureDetectorBehavior; final HitTestBehavior gestureDetectorBehavior;
@ -66,9 +63,6 @@ class _MagnifierState extends State<Magnifier> {
bool _controlledController; bool _controlledController;
MagnifierController _controller; MagnifierController _controller;
bool _controlledScaleStateController;
MagnifierScaleStateController _scaleStateController;
void _setChildSize(Size childSize) { void _setChildSize(Size childSize) {
_childSize = childSize.isEmpty ? null : childSize; _childSize = childSize.isEmpty ? null : childSize;
} }
@ -84,14 +78,6 @@ class _MagnifierState extends State<Magnifier> {
_controlledController = false; _controlledController = false;
_controller = widget.controller; _controller = widget.controller;
} }
if (widget.scaleStateController == null) {
_controlledScaleStateController = true;
_scaleStateController = MagnifierScaleStateController();
} else {
_controlledScaleStateController = false;
_scaleStateController = widget.scaleStateController;
}
} }
@override @override
@ -110,16 +96,6 @@ class _MagnifierState extends State<Magnifier> {
_controlledController = false; _controlledController = false;
_controller = widget.controller; _controller = widget.controller;
} }
if (widget.scaleStateController == null) {
if (!_controlledScaleStateController) {
_controlledScaleStateController = true;
_scaleStateController = MagnifierScaleStateController();
}
} else {
_controlledScaleStateController = false;
_scaleStateController = widget.scaleStateController;
}
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
} }
@ -128,9 +104,6 @@ class _MagnifierState extends State<Magnifier> {
if (_controlledController) { if (_controlledController) {
_controller.dispose(); _controller.dispose();
} }
if (_controlledScaleStateController) {
_scaleStateController.dispose();
}
super.dispose(); super.dispose();
} }
@ -138,20 +111,18 @@ class _MagnifierState extends State<Magnifier> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return LayoutBuilder( return LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
final scaleBoundaries = ScaleBoundaries( _controller.setScaleBoundaries(ScaleBoundaries(
widget.minScale ?? 0.0, widget.minScale ?? 0.0,
widget.maxScale ?? ScaleLevel(factor: double.infinity), widget.maxScale ?? ScaleLevel(factor: double.infinity),
widget.initialScale ?? ScaleLevel(ref: ScaleReference.contained), widget.initialScale ?? ScaleLevel(ref: ScaleReference.contained),
constraints.biggest, constraints.biggest,
_childSize ?? constraints.biggest, _childSize ?? constraints.biggest,
); ));
return MagnifierCore( return MagnifierCore(
child: widget.child, child: widget.child,
controller: _controller, controller: _controller,
scaleStateController: _scaleStateController,
scaleStateCycle: widget.scaleStateCycle ?? defaultScaleStateCycle, scaleStateCycle: widget.scaleStateCycle ?? defaultScaleStateCycle,
scaleBoundaries: scaleBoundaries,
onTap: widget.onTap, onTap: widget.onTap,
gestureDetectorBehavior: widget.gestureDetectorBehavior, gestureDetectorBehavior: widget.gestureDetectorBehavior,
applyScale: widget.applyScale ?? true, applyScale: widget.applyScale ?? true,

View file

@ -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<ScaleStateChange> _outputScaleStateCtrl;
ScaleStateChange prevScaleState;
Stream<ScaleStateChange> 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<ScaleStateChange>.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);
}
}

View file

@ -177,9 +177,9 @@ class _AlbumFilterBarState extends State<AlbumFilterBar> {
), ),
ConstrainedBox( ConstrainedBox(
constraints: BoxConstraints(minWidth: 16), constraints: BoxConstraints(minWidth: 16),
child: AnimatedBuilder( child: ValueListenableBuilder<TextEditingValue>(
animation: _controller, valueListenable: _controller,
builder: (context, child) => AnimatedSwitcher( builder: (context, value, child) => AnimatedSwitcher(
duration: Durations.appBarActionChangeAnimation, duration: Durations.appBarActionChangeAnimation,
transitionBuilder: (child, animation) => FadeTransition( transitionBuilder: (child, animation) => FadeTransition(
opacity: animation, opacity: animation,
@ -189,7 +189,7 @@ class _AlbumFilterBarState extends State<AlbumFilterBar> {
child: child, child: child,
), ),
), ),
child: _controller.text.isNotEmpty ? clearButton : SizedBox.shrink(), child: value.text.isNotEmpty ? clearButton : SizedBox.shrink(),
), ),
), ),
) )

View file

@ -1,16 +1,19 @@
import 'dart:async'; import 'dart:async';
import 'dart:math';
import 'package:aves/image_providers/thumbnail_provider.dart'; import 'package:aves/image_providers/thumbnail_provider.dart';
import 'package:aves/image_providers/uri_picture_provider.dart'; import 'package:aves/image_providers/uri_picture_provider.dart';
import 'package:aves/model/image_entry.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/model/settings/settings.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/collection/empty.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/controller.dart';
import 'package:aves/widgets/common/magnifier/controller/state.dart'; import 'package:aves/widgets/common/magnifier/controller/state.dart';
import 'package:aves/widgets/common/magnifier/magnifier.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/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/common/magnifier/scale/state.dart';
import 'package:aves/widgets/fullscreen/tiled_view.dart'; import 'package:aves/widgets/fullscreen/tiled_view.dart';
import 'package:aves/widgets/fullscreen/video_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/material.dart';
import 'package:flutter_ijkplayer/flutter_ijkplayer.dart'; import 'package:flutter_ijkplayer/flutter_ijkplayer.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:provider/provider.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
class ImageView extends StatefulWidget { class ImageView extends StatefulWidget {
@ -43,10 +45,8 @@ class ImageView extends StatefulWidget {
class _ImageViewState extends State<ImageView> { class _ImageViewState extends State<ImageView> {
final MagnifierController _magnifierController = MagnifierController(); final MagnifierController _magnifierController = MagnifierController();
final MagnifierScaleStateController _magnifierScaleStateController = MagnifierScaleStateController();
final ValueNotifier<ViewState> _viewStateNotifier = ValueNotifier(ViewState.zero); final ValueNotifier<ViewState> _viewStateNotifier = ValueNotifier(ViewState.zero);
StreamSubscription<MagnifierState> _subscription; final List<StreamSubscription> _subscriptions = [];
Size _magnifierChildSize;
static const initialScale = ScaleLevel(ref: ScaleReference.contained); static const initialScale = ScaleLevel(ref: ScaleReference.contained);
static const minScale = ScaleLevel(ref: ScaleReference.contained); static const minScale = ScaleLevel(ref: ScaleReference.contained);
@ -56,17 +56,20 @@ class _ImageViewState extends State<ImageView> {
MagnifierTapCallback get onTap => widget.onTap; MagnifierTapCallback get onTap => widget.onTap;
static const decorationCheckSize = 20.0;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_subscription = _magnifierController.outputStateStream.listen(_onViewChanged); _subscriptions.add(_magnifierController.stateStream.listen(_onViewStateChanged));
_magnifierChildSize = entry.displaySize; _subscriptions.add(_magnifierController.scaleBoundariesStream.listen(_onViewScaleBoundariesChanged));
} }
@override @override
void dispose() { void dispose() {
_subscription.cancel(); _subscriptions
_subscription = null; ..forEach((sub) => sub.cancel())
..clear();
widget.onDisposed?.call(); widget.onDisposed?.call();
super.dispose(); super.dispose();
} }
@ -118,30 +121,14 @@ class _ImageViewState extends State<ImageView> {
return Magnifier( return Magnifier(
// key includes size and orientation to refresh when the image is rotated // key includes size and orientation to refresh when the image is rotated
key: ValueKey('${entry.rotationDegrees}_${entry.isFlipped}_${entry.width}_${entry.height}_${entry.path}'), key: ValueKey('${entry.rotationDegrees}_${entry.isFlipped}_${entry.width}_${entry.height}_${entry.path}'),
child: Selector<MediaQueryData, Size>( child: TiledImageView(
selector: (context, mq) => mq.size, entry: entry,
builder: (context, mqSize, child) { viewStateNotifier: _viewStateNotifier,
// When the scale state is cycled to be in its `initial` state (i.e. `contained`), and the device is rotated, baseChild: _loadingBuilder(context, fastThumbnailProvider),
// `Magnifier` keeps the scale state as `contained`, but the controller does not update or notify the new scale value. errorBuilder: (context, error, stackTrace) => ErrorChild(onTap: () => onTap?.call(null)),
// 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)),
);
},
), ),
childSize: entry.displaySize, childSize: entry.displaySize,
controller: _magnifierController, controller: _magnifierController,
scaleStateController: _magnifierScaleStateController,
maxScale: maxScale, maxScale: maxScale,
minScale: minScale, minScale: minScale,
initialScale: initialScale, initialScale: initialScale,
@ -151,8 +138,10 @@ class _ImageViewState extends State<ImageView> {
} }
Widget _buildSvgView() { Widget _buildSvgView() {
final colorFilter = ColorFilter.mode(Color(settings.svgBackground), BlendMode.dstOver); final background = settings.vectorBackground;
return Magnifier( final colorFilter = background.isColor ? ColorFilter.mode(background.color, BlendMode.dstOver) : null;
Widget child = Magnifier(
child: SvgPicture( child: SvgPicture(
UriPicture( UriPicture(
uri: entry.uri, uri: entry.uri,
@ -167,6 +156,42 @@ class _ImageViewState extends State<ImageView> {
scaleStateCycle: _vectorScaleStateCycle, scaleStateCycle: _vectorScaleStateCycle,
onTap: (c, d, s, childPosition) => onTap?.call(childPosition), onTap: (c, d, s, childPosition) => onTap?.call(childPosition),
); );
if (background == EntryBackground.checkered) {
child = ValueListenableBuilder<ViewState>(
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() { Widget _buildVideoView() {
@ -187,8 +212,16 @@ class _ImageViewState extends State<ImageView> {
); );
} }
void _onViewChanged(MagnifierState v) { void _onViewStateChanged(MagnifierState v) {
final viewState = ViewState(v.position, v.scale, _magnifierChildSize); 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; _viewStateNotifier.value = viewState;
ViewStateNotification(entry.uri, viewState).dispatch(context); ViewStateNotification(entry.uri, viewState).dispatch(context);
} }
@ -206,14 +239,14 @@ class _ImageViewState extends State<ImageView> {
class ViewState { class ViewState {
final Offset position; final Offset position;
final double scale; 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 @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 { class ViewStateNotification extends Notification {

View file

@ -92,9 +92,9 @@ class _MetadataSectionSliverState extends State<MetadataSectionSliver> with Auto
// cancel notification bubbling so that the info page // cancel notification bubbling so that the info page
// does not misinterpret content scrolling for page scrolling // does not misinterpret content scrolling for page scrolling
onNotification: (notification) => true, onNotification: (notification) => true,
child: AnimatedBuilder( child: ValueListenableBuilder<String>(
animation: _loadedMetadataUri, valueListenable: _loadedMetadataUri,
builder: (context, child) { builder: (context, uri, child) {
Widget content; Widget content;
if (_metadata.isEmpty) { if (_metadata.isEmpty) {
content = SizedBox.shrink(); content = SizedBox.shrink();
@ -119,7 +119,7 @@ class _MetadataSectionSliverState extends State<MetadataSectionSliver> with Auto
return AnimationLimiter( return AnimationLimiter(
// we update the limiter key after fetching the metadata of a new entry, // we update the limiter key after fetching the metadata of a new entry,
// in order to restart the staggered animation of the metadata section // in order to restart the staggered animation of the metadata section
key: Key(_loadedMetadataUri.value), key: Key(uri),
child: content, child: content,
); );
}, },

View file

@ -4,7 +4,6 @@ import 'package:aves/model/image_entry.dart';
import 'package:aves/widgets/fullscreen/image_view.dart'; import 'package:aves/widgets/fullscreen/image_view.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class Minimap extends StatelessWidget { class Minimap extends StatelessWidget {
final ImageEntry entry; final ImageEntry entry;
@ -22,24 +21,21 @@ class Minimap extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return IgnorePointer( return IgnorePointer(
child: Selector<MediaQueryData, Size>( child: ValueListenableBuilder<ViewState>(
selector: (context, mq) => mq.size, valueListenable: viewStateNotifier,
builder: (context, mqSize, child) { builder: (context, viewState, child) {
return AnimatedBuilder( final viewportSize = viewState.viewportSize;
animation: viewStateNotifier, if (viewportSize == null) return SizedBox.shrink();
builder: (context, child) { return CustomPaint(
final viewState = viewStateNotifier.value; painter: MinimapPainter(
return CustomPaint( viewportSize: viewportSize,
painter: MinimapPainter( entrySize: entry.displaySize,
viewportSize: mqSize, viewCenterOffset: viewState.position,
entrySize: viewState.size ?? entry.displaySize, viewScale: viewState.scale,
viewCenterOffset: viewState.position, minimapBorderColor: Colors.white30,
viewScale: viewState.scale, ),
minimapBorderColor: Colors.white30, size: size,
), );
size: size,
);
});
}), }),
); );
} }

View file

@ -10,14 +10,12 @@ import 'package:flutter/material.dart';
class TiledImageView extends StatefulWidget { class TiledImageView extends StatefulWidget {
final ImageEntry entry; final ImageEntry entry;
final Size viewportSize;
final ValueNotifier<ViewState> viewStateNotifier; final ValueNotifier<ViewState> viewStateNotifier;
final Widget baseChild; final Widget baseChild;
final ImageErrorWidgetBuilder errorBuilder; final ImageErrorWidgetBuilder errorBuilder;
const TiledImageView({ const TiledImageView({
@required this.entry, @required this.entry,
@required this.viewportSize,
@required this.viewStateNotifier, @required this.viewStateNotifier,
@required this.baseChild, @required this.baseChild,
@required this.errorBuilder, @required this.errorBuilder,
@ -28,14 +26,13 @@ class TiledImageView extends StatefulWidget {
} }
class _TiledImageViewState extends State<TiledImageView> { class _TiledImageViewState extends State<TiledImageView> {
bool _initialized = false;
double _tileSide, _initialScale; double _tileSide, _initialScale;
int _maxSampleSize; int _maxSampleSize;
Matrix4 _transform; Matrix4 _transform;
ImageEntry get entry => widget.entry; ImageEntry get entry => widget.entry;
Size get viewportSize => widget.viewportSize;
ValueNotifier<ViewState> get viewStateNotifier => widget.viewStateNotifier; ValueNotifier<ViewState> get viewStateNotifier => widget.viewStateNotifier;
bool get useTiles => entry.canTile && (entry.width > 4096 || entry.height > 4096); bool get useTiles => entry.canTile && (entry.width > 4096 || entry.height > 4096);
@ -51,24 +48,21 @@ class _TiledImageViewState extends State<TiledImageView> {
// magic number used to derive sample size from scale // magic number used to derive sample size from scale
static const scaleFactor = 2.0; static const scaleFactor = 2.0;
@override
void initState() {
super.initState();
_init();
}
@override @override
void didUpdateWidget(TiledImageView oldWidget) { void didUpdateWidget(TiledImageView oldWidget) {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
if (oldWidget.viewportSize != widget.viewportSize || oldWidget.entry.displaySize != widget.entry.displaySize) { final oldViewState = oldWidget.viewStateNotifier.value;
_init(); 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; _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); _maxSampleSize = _sampleSizeForScale(_initialScale);
final rotationDegrees = entry.rotationDegrees; final rotationDegrees = entry.rotationDegrees;
@ -79,8 +73,9 @@ class _TiledImageViewState extends State<TiledImageView> {
..translate(entry.width / 2.0, entry.height / 2.0) ..translate(entry.width / 2.0, entry.height / 2.0)
..scale(isFlipped ? -1.0 : 1.0, 1.0, 1.0) ..scale(isFlipped ? -1.0 : 1.0, 1.0, 1.0)
..rotateZ(-toRadians(rotationDegrees.toDouble())) ..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 @override
@ -90,10 +85,13 @@ class _TiledImageViewState extends State<TiledImageView> {
final displayWidth = entry.displaySize.width.round(); final displayWidth = entry.displaySize.width.round();
final displayHeight = entry.displaySize.height.round(); final displayHeight = entry.displaySize.height.round();
return AnimatedBuilder( return ValueListenableBuilder<ViewState>(
animation: viewStateNotifier, valueListenable: viewStateNotifier,
builder: (context, child) { builder: (context, viewState, child) {
final viewState = viewStateNotifier.value; final viewportSize = viewState.viewportSize;
if (viewportSize == null) return SizedBox.shrink();
if (!_initialized) _initFromViewport(viewportSize);
var scale = viewState.scale; var scale = viewState.scale;
if (scale == 0.0) { if (scale == 0.0) {
// for initial scale as `contained` // for initial scale as `contained`
@ -136,6 +134,7 @@ class _TiledImageViewState extends State<TiledImageView> {
List<RegionTile> _getTiles(ViewState viewState, int displayWidth, int displayHeight, double scale) { List<RegionTile> _getTiles(ViewState viewState, int displayWidth, int displayHeight, double scale) {
final centerOffset = viewState.position; final centerOffset = viewState.position;
final viewportSize = viewState.viewportSize;
final viewOrigin = Offset( final viewOrigin = Offset(
((displayWidth * scale - viewportSize.width) / 2 - centerOffset.dx), ((displayWidth * scale - viewportSize.width) / 2 - centerOffset.dx),
((displayHeight * scale - viewportSize.height) / 2 - centerOffset.dy), ((displayHeight * scale - viewportSize.height) / 2 - centerOffset.dy),

View file

@ -1,5 +1,7 @@
import 'package:aves/model/settings/entry_background.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/widgets/common/fx/borders.dart'; import 'package:aves/widgets/common/fx/borders.dart';
import 'package:aves/widgets/common/fx/checkered_decoration.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class SvgBackgroundSelector extends StatefulWidget { class SvgBackgroundSelector extends StatefulWidget {
@ -10,33 +12,53 @@ class SvgBackgroundSelector extends StatefulWidget {
class _SvgBackgroundSelectorState extends State<SvgBackgroundSelector> { class _SvgBackgroundSelectorState extends State<SvgBackgroundSelector> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
const radius = 24.0; const radius = 12.0;
return DropdownButtonHideUnderline( return DropdownButtonHideUnderline(
child: DropdownButton<int>( child: DropdownButton<EntryBackground>(
items: [0xFFFFFFFF, 0xFF000000, 0x00000000].map((selected) { items: [
return DropdownMenuItem<int>( 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<EntryBackground>(
value: selected, value: selected,
child: Container( child: Container(
height: radius, height: radius * 2,
width: radius, width: radius * 2,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Color(selected), color: selected.isColor ? selected.color : null,
border: AvesCircleBorder.build(context), border: AvesCircleBorder.build(context),
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
child: selected == 0 child: child,
? Icon(
Icons.clear,
size: 20,
color: Colors.white30,
)
: null,
), ),
); );
}).toList(), }).toList(),
value: settings.svgBackground, value: settings.vectorBackground,
onChanged: (selected) { onChanged: (selected) {
settings.svgBackground = selected; settings.vectorBackground = selected;
setState(() {}); setState(() {});
}, },
), ),