svg: optional checkered background
This commit is contained in:
parent
c9fb94f326
commit
b14558e451
17 changed files with 443 additions and 368 deletions
26
lib/model/settings/entry_background.dart
Normal file
26
lib/model/settings/entry_background.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -43,7 +43,6 @@ class DecoratedThumbnail extends StatelessWidget {
|
||||||
);
|
);
|
||||||
|
|
||||||
child = Stack(
|
child = Stack(
|
||||||
fit: StackFit.passthrough,
|
|
||||||
children: [
|
children: [
|
||||||
child,
|
child,
|
||||||
Positioned(
|
Positioned(
|
||||||
|
|
|
@ -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,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
57
lib/widgets/common/fx/checkered_decoration.dart
Normal file
57
lib/widgets/common/fx/checkered_decoration.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -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,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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(() {});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
Loading…
Reference in a new issue