mirror of
https://github.com/vodemn/m3_lightmeter.git
synced 2024-11-21 23:10:40 +00:00
Implemented metering dialog picker
typo fixed closed container scale fixed dialog title animation implemented `AnimatedDialog` added open/close children transition clean up close dialog
This commit is contained in:
parent
7c20bfe80d
commit
00c0a6134d
12 changed files with 528 additions and 289 deletions
|
@ -23,8 +23,10 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||||
final messages = _notInlinedMessages(_notInlinedMessages);
|
final messages = _notInlinedMessages(_notInlinedMessages);
|
||||||
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
|
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
|
||||||
"caffeine": MessageLookupByLibrary.simpleMessage("Caffeine"),
|
"caffeine": MessageLookupByLibrary.simpleMessage("Caffeine"),
|
||||||
|
"cancel": MessageLookupByLibrary.simpleMessage("Cancel"),
|
||||||
"fastestExposurePair": MessageLookupByLibrary.simpleMessage("Fastest"),
|
"fastestExposurePair": MessageLookupByLibrary.simpleMessage("Fastest"),
|
||||||
"haptics": MessageLookupByLibrary.simpleMessage("Haptics"),
|
"haptics": MessageLookupByLibrary.simpleMessage("Haptics"),
|
||||||
|
"iso": MessageLookupByLibrary.simpleMessage("ISO"),
|
||||||
"keepsScreenOn":
|
"keepsScreenOn":
|
||||||
MessageLookupByLibrary.simpleMessage("Keeps screen on"),
|
MessageLookupByLibrary.simpleMessage("Keeps screen on"),
|
||||||
"openSettings": MessageLookupByLibrary.simpleMessage("Open settings"),
|
"openSettings": MessageLookupByLibrary.simpleMessage("Open settings"),
|
||||||
|
@ -32,6 +34,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||||
MessageLookupByLibrary.simpleMessage("Permission needed"),
|
MessageLookupByLibrary.simpleMessage("Permission needed"),
|
||||||
"permissionNeededMessage": MessageLookupByLibrary.simpleMessage(
|
"permissionNeededMessage": MessageLookupByLibrary.simpleMessage(
|
||||||
"To use Lightmeter, turn on Camera permissions."),
|
"To use Lightmeter, turn on Camera permissions."),
|
||||||
|
"select": MessageLookupByLibrary.simpleMessage("Select"),
|
||||||
"settings": MessageLookupByLibrary.simpleMessage("Settings"),
|
"settings": MessageLookupByLibrary.simpleMessage("Settings"),
|
||||||
"slowestExposurePair": MessageLookupByLibrary.simpleMessage("Slowest")
|
"slowestExposurePair": MessageLookupByLibrary.simpleMessage("Slowest")
|
||||||
};
|
};
|
||||||
|
|
|
@ -100,6 +100,36 @@ class S {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `ISO`
|
||||||
|
String get iso {
|
||||||
|
return Intl.message(
|
||||||
|
'ISO',
|
||||||
|
name: 'iso',
|
||||||
|
desc: '',
|
||||||
|
args: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Cancel`
|
||||||
|
String get cancel {
|
||||||
|
return Intl.message(
|
||||||
|
'Cancel',
|
||||||
|
name: 'cancel',
|
||||||
|
desc: '',
|
||||||
|
args: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Select`
|
||||||
|
String get select {
|
||||||
|
return Intl.message(
|
||||||
|
'Select',
|
||||||
|
name: 'select',
|
||||||
|
desc: '',
|
||||||
|
args: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// `Settings`
|
/// `Settings`
|
||||||
String get settings {
|
String get settings {
|
||||||
return Intl.message(
|
return Intl.message(
|
||||||
|
|
|
@ -5,6 +5,9 @@
|
||||||
"openSettings": "Open settings",
|
"openSettings": "Open settings",
|
||||||
"fastestExposurePair": "Fastest",
|
"fastestExposurePair": "Fastest",
|
||||||
"slowestExposurePair": "Slowest",
|
"slowestExposurePair": "Slowest",
|
||||||
|
"iso": "ISO",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"select": "Select",
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
"caffeine": "Caffeine",
|
"caffeine": "Caffeine",
|
||||||
"keepsScreenOn": "Keeps screen on",
|
"keepsScreenOn": "Keeps screen on",
|
||||||
|
|
|
@ -13,7 +13,6 @@ import 'res/dimens.dart';
|
||||||
import 'res/theme.dart';
|
import 'res/theme.dart';
|
||||||
import 'screens/metering/metering_bloc.dart';
|
import 'screens/metering/metering_bloc.dart';
|
||||||
import 'screens/metering/metering_screen.dart';
|
import 'screens/metering/metering_screen.dart';
|
||||||
import 'screens/permissions_check/flow_permissions_check.dart';
|
|
||||||
import 'utils/stop_type_provider.dart';
|
import 'utils/stop_type_provider.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
|
|
@ -0,0 +1,95 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:lightmeter/generated/l10n.dart';
|
||||||
|
import 'package:lightmeter/res/dimens.dart';
|
||||||
|
|
||||||
|
class MeteringScreenDialogPicker<T> extends StatefulWidget {
|
||||||
|
final String title;
|
||||||
|
final T initialValue;
|
||||||
|
final List<T> values;
|
||||||
|
final Widget Function(BuildContext context, T value) itemTitleBuilder;
|
||||||
|
final VoidCallback onCancel;
|
||||||
|
final ValueChanged onSelect;
|
||||||
|
|
||||||
|
const MeteringScreenDialogPicker({
|
||||||
|
required this.title,
|
||||||
|
required this.initialValue,
|
||||||
|
required this.values,
|
||||||
|
required this.itemTitleBuilder,
|
||||||
|
required this.onCancel,
|
||||||
|
required this.onSelect,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MeteringScreenDialogPicker<T>> createState() => _MeteringScreenDialogPickerState<T>();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MeteringScreenDialogPickerState<T> extends State<MeteringScreenDialogPicker<T>> {
|
||||||
|
late T _selectedValue = widget.initialValue;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(
|
||||||
|
Dimens.paddingL,
|
||||||
|
Dimens.paddingL,
|
||||||
|
Dimens.paddingL,
|
||||||
|
Dimens.paddingM,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
widget.title,
|
||||||
|
style: Theme.of(context).textTheme.headlineSmall!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Divider(
|
||||||
|
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||||
|
height: 0,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: ListView.builder(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
itemCount: widget.values.length,
|
||||||
|
itemBuilder: (context, index) => RadioListTile(
|
||||||
|
value: widget.values[index],
|
||||||
|
groupValue: _selectedValue,
|
||||||
|
title: widget.itemTitleBuilder(context, widget.values[index]),
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value != null) {
|
||||||
|
setState(() {
|
||||||
|
_selectedValue = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Divider(
|
||||||
|
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||||
|
height: 0,
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(Dimens.paddingL),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
children: [
|
||||||
|
const Spacer(),
|
||||||
|
TextButton(
|
||||||
|
onPressed: widget.onCancel,
|
||||||
|
child: Text(S.of(context).cancel),
|
||||||
|
),
|
||||||
|
const SizedBox(width: Dimens.grid16),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => widget.onSelect(_selectedValue),
|
||||||
|
child: Text(S.of(context).select),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,212 +1,6 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
|
||||||
import 'package:lightmeter/res/dimens.dart';
|
import 'package:lightmeter/res/dimens.dart';
|
||||||
|
import 'package:lightmeter/screens/metering/components/topbar/models/reading_value.dart';
|
||||||
class ReadingValue {
|
|
||||||
final String label;
|
|
||||||
final String value;
|
|
||||||
|
|
||||||
const ReadingValue({
|
|
||||||
required this.label,
|
|
||||||
required this.value,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
class ReadingContainerWithDialog extends StatefulWidget {
|
|
||||||
final ReadingValue value;
|
|
||||||
final Widget Function(BuildContext context) dialogBuilder;
|
|
||||||
|
|
||||||
const ReadingContainerWithDialog({
|
|
||||||
required this.value,
|
|
||||||
required this.dialogBuilder,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<ReadingContainerWithDialog> createState() => _ReadingContainerWithDialogState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ReadingContainerWithDialogState extends State<ReadingContainerWithDialog> with SingleTickerProviderStateMixin {
|
|
||||||
final GlobalKey _key = GlobalKey();
|
|
||||||
final LayerLink _layerLink = LayerLink();
|
|
||||||
OverlayEntry? _overlayEntry;
|
|
||||||
|
|
||||||
late final _animationController = AnimationController(
|
|
||||||
duration: Dimens.durationL,
|
|
||||||
reverseDuration: Dimens.durationML,
|
|
||||||
vsync: this,
|
|
||||||
);
|
|
||||||
late final _defaultCurve = CurvedAnimation(parent: _animationController, curve: Curves.linear);
|
|
||||||
|
|
||||||
late final _colorAnimation = ColorTween(
|
|
||||||
begin: Colors.transparent,
|
|
||||||
end: Colors.black54,
|
|
||||||
).animate(_defaultCurve);
|
|
||||||
late final _borderRadiusAnimation = Tween<double>(
|
|
||||||
begin: Dimens.borderRadiusM,
|
|
||||||
end: Dimens.borderRadiusXL,
|
|
||||||
).animate(_defaultCurve);
|
|
||||||
late final _itemOpacityAnimation = Tween<double>(
|
|
||||||
begin: 1,
|
|
||||||
end: 0,
|
|
||||||
).animate(CurvedAnimation(
|
|
||||||
parent: _animationController,
|
|
||||||
curve: const Interval(0, 0.5, curve: Curves.linear),
|
|
||||||
));
|
|
||||||
late final _dialogOpacityAnimation = Tween<double>(
|
|
||||||
begin: 0,
|
|
||||||
end: 1,
|
|
||||||
).animate(CurvedAnimation(
|
|
||||||
parent: _animationController,
|
|
||||||
curve: const Interval(0.5, 1.0, curve: Curves.linear),
|
|
||||||
));
|
|
||||||
|
|
||||||
late final SizeTween _sizeTween;
|
|
||||||
late final Animation<Size?> _sizeAnimation;
|
|
||||||
late final Animation<Size?> _offsetAnimation;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
//timeDilation = 5.0;
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
final mediaQuery = MediaQuery.of(context);
|
|
||||||
final itemWidth = _key.currentContext!.size!.width;
|
|
||||||
final itemHeight = _key.currentContext!.size!.height;
|
|
||||||
|
|
||||||
_sizeTween = SizeTween(
|
|
||||||
begin: Size(
|
|
||||||
itemWidth,
|
|
||||||
itemHeight,
|
|
||||||
),
|
|
||||||
end: Size(
|
|
||||||
mediaQuery.size.width - mediaQuery.padding.horizontal - Dimens.paddingL * 2,
|
|
||||||
mediaQuery.size.height - mediaQuery.padding.vertical - Dimens.paddingL * 2,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
_sizeAnimation = _sizeTween.animate(_defaultCurve);
|
|
||||||
|
|
||||||
final renderBox = _key.currentContext!.findRenderObject() as RenderBox;
|
|
||||||
final offset = renderBox.localToGlobal(Offset.zero);
|
|
||||||
_offsetAnimation = SizeTween(
|
|
||||||
begin: Size(
|
|
||||||
offset.dx + itemWidth / 2,
|
|
||||||
offset.dy + itemHeight / 2,
|
|
||||||
),
|
|
||||||
end: Size(
|
|
||||||
mediaQuery.size.width / 2,
|
|
||||||
mediaQuery.size.height / 2 + mediaQuery.padding.top / 2 - mediaQuery.padding.bottom / 2,
|
|
||||||
),
|
|
||||||
).animate(_defaultCurve);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_animationController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return InkWell(
|
|
||||||
key: _key,
|
|
||||||
onTap: _openDialog,
|
|
||||||
child: CompositedTransformTarget(
|
|
||||||
link: _layerLink,
|
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(Dimens.borderRadiusM),
|
|
||||||
child: ColoredBox(
|
|
||||||
color: Theme.of(context).colorScheme.primaryContainer,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(Dimens.paddingM),
|
|
||||||
child: _ReadingValueBuilder(widget.value),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _openDialog() {
|
|
||||||
final RenderBox renderBox = _key.currentContext!.findRenderObject() as RenderBox;
|
|
||||||
final offset = renderBox.localToGlobal(Offset.zero);
|
|
||||||
_overlayEntry = OverlayEntry(
|
|
||||||
builder: (context) => CompositedTransformFollower(
|
|
||||||
offset: Offset(-offset.dx, -offset.dy),
|
|
||||||
link: _layerLink,
|
|
||||||
showWhenUnlinked: false,
|
|
||||||
child: SizedBox.fromSize(
|
|
||||||
size: MediaQuery.of(context).size,
|
|
||||||
child: AnimatedBuilder(
|
|
||||||
animation: _animationController,
|
|
||||||
builder: (context, _) => Stack(
|
|
||||||
children: [
|
|
||||||
Positioned.fill(
|
|
||||||
child: GestureDetector(
|
|
||||||
onTap: _closeDialog,
|
|
||||||
child: ColoredBox(color: _colorAnimation.value!),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Positioned.fromRect(
|
|
||||||
rect: Rect.fromCenter(
|
|
||||||
center: Offset(
|
|
||||||
_offsetAnimation.value!.width,
|
|
||||||
_offsetAnimation.value!.height,
|
|
||||||
),
|
|
||||||
width: _sizeAnimation.value!.width,
|
|
||||||
height: _sizeAnimation.value!.height,
|
|
||||||
),
|
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(_borderRadiusAnimation.value),
|
|
||||||
child: ColoredBox(
|
|
||||||
color: Theme.of(context).colorScheme.primaryContainer,
|
|
||||||
child: Center(
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
Opacity(
|
|
||||||
opacity: _itemOpacityAnimation.value,
|
|
||||||
child: Transform.scale(
|
|
||||||
scale: _sizeAnimation.value!.width / _sizeTween.begin!.width,
|
|
||||||
child: SizedBox(
|
|
||||||
width: _sizeTween.begin!.width,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(Dimens.paddingM),
|
|
||||||
child: _ReadingValueBuilder(widget.value),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Opacity(
|
|
||||||
opacity: _dialogOpacityAnimation.value,
|
|
||||||
child: Transform.scale(
|
|
||||||
scale: _sizeAnimation.value!.width / _sizeTween.end!.width,
|
|
||||||
child: widget.dialogBuilder(context),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
Overlay.of(context)?.insert(_overlayEntry!);
|
|
||||||
_animationController.forward();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _closeDialog() {
|
|
||||||
_animationController.reverse();
|
|
||||||
Future.delayed(_animationController.reverseDuration! * timeDilation).then((_) {
|
|
||||||
_overlayEntry?.remove();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ReadingContainer extends StatelessWidget {
|
class ReadingContainer extends StatelessWidget {
|
||||||
final List<_ReadingValueBuilder> _items;
|
final List<_ReadingValueBuilder> _items;
|
||||||
|
|
|
@ -0,0 +1,283 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/scheduler.dart';
|
||||||
|
import 'package:lightmeter/res/dimens.dart';
|
||||||
|
|
||||||
|
class AnimatedDialog extends StatefulWidget {
|
||||||
|
final Size? openedSize;
|
||||||
|
final Widget? closedChild;
|
||||||
|
final Widget? openedChild;
|
||||||
|
final Widget? child;
|
||||||
|
|
||||||
|
const AnimatedDialog({
|
||||||
|
this.openedSize,
|
||||||
|
this.closedChild,
|
||||||
|
this.openedChild,
|
||||||
|
this.child,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AnimatedDialog> createState() => AnimatedDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class AnimatedDialogState extends State<AnimatedDialog> with SingleTickerProviderStateMixin {
|
||||||
|
final GlobalKey _key = GlobalKey();
|
||||||
|
final LayerLink _layerLink = LayerLink();
|
||||||
|
|
||||||
|
late final Size _closedSize;
|
||||||
|
late final Offset _closedOffset;
|
||||||
|
|
||||||
|
late final AnimationController _animationController;
|
||||||
|
late final CurvedAnimation _defaultCurvedAnimation;
|
||||||
|
late final Animation<Color?> _barrierColorAnimation;
|
||||||
|
late final SizeTween _sizeTween;
|
||||||
|
late final Animation<Size?> _sizeAnimation;
|
||||||
|
late final Animation<Size?> _offsetAnimation;
|
||||||
|
late final Animation<double> _borderRadiusAnimation;
|
||||||
|
late final Animation<double> _closedOpacityAnimation;
|
||||||
|
late final Animation<double> _openedOpacityAnimation;
|
||||||
|
OverlayEntry? _overlayEntry;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
//timeDilation = 10.0;
|
||||||
|
_animationController = AnimationController(
|
||||||
|
duration: Dimens.durationL,
|
||||||
|
reverseDuration: Dimens.durationML,
|
||||||
|
vsync: this,
|
||||||
|
);
|
||||||
|
_defaultCurvedAnimation = CurvedAnimation(parent: _animationController, curve: Curves.easeInOut);
|
||||||
|
_barrierColorAnimation = ColorTween(
|
||||||
|
begin: Colors.transparent,
|
||||||
|
end: Colors.black54,
|
||||||
|
).animate(_defaultCurvedAnimation);
|
||||||
|
_borderRadiusAnimation = Tween<double>(
|
||||||
|
begin: Dimens.borderRadiusM,
|
||||||
|
end: Dimens.borderRadiusXL,
|
||||||
|
).animate(_defaultCurvedAnimation);
|
||||||
|
|
||||||
|
_closedOpacityAnimation = Tween<double>(
|
||||||
|
begin: 1,
|
||||||
|
end: 0,
|
||||||
|
).animate(CurvedAnimation(
|
||||||
|
parent: _animationController,
|
||||||
|
curve: const Interval(
|
||||||
|
0,
|
||||||
|
0.8,
|
||||||
|
curve: Curves.ease,
|
||||||
|
),
|
||||||
|
));
|
||||||
|
_openedOpacityAnimation = Tween<double>(
|
||||||
|
begin: 0,
|
||||||
|
end: 1,
|
||||||
|
).animate(CurvedAnimation(
|
||||||
|
parent: _animationController,
|
||||||
|
curve: const Interval(
|
||||||
|
0.8,
|
||||||
|
1.0,
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||||
|
final mediaQuery = MediaQuery.of(context);
|
||||||
|
|
||||||
|
_closedSize = _key.currentContext!.size!;
|
||||||
|
_sizeTween = SizeTween(
|
||||||
|
begin: _closedSize,
|
||||||
|
end: widget.openedSize ??
|
||||||
|
Size(
|
||||||
|
mediaQuery.size.width - mediaQuery.padding.horizontal - Dimens.paddingM * 4,
|
||||||
|
mediaQuery.size.height - mediaQuery.padding.vertical - Dimens.paddingM * 4,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
_sizeAnimation = _sizeTween.animate(_defaultCurvedAnimation);
|
||||||
|
|
||||||
|
final renderBox = _key.currentContext!.findRenderObject() as RenderBox;
|
||||||
|
_closedOffset = renderBox.localToGlobal(Offset.zero);
|
||||||
|
_offsetAnimation = SizeTween(
|
||||||
|
begin: Size(
|
||||||
|
_closedOffset.dx + _closedSize.width / 2,
|
||||||
|
_closedOffset.dy + _closedSize.height / 2,
|
||||||
|
),
|
||||||
|
end: Size(
|
||||||
|
mediaQuery.size.width / 2,
|
||||||
|
mediaQuery.size.height / 2 + mediaQuery.padding.top / 2 - mediaQuery.padding.bottom / 2,
|
||||||
|
),
|
||||||
|
).animate(_defaultCurvedAnimation);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_animationController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return InkWell(
|
||||||
|
key: _key,
|
||||||
|
onTap: _openDialog,
|
||||||
|
child: CompositedTransformTarget(
|
||||||
|
link: _layerLink,
|
||||||
|
child: Opacity(
|
||||||
|
opacity: _overlayEntry != null ? 0 : 1,
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(Dimens.borderRadiusM),
|
||||||
|
child: ColoredBox(
|
||||||
|
color: Theme.of(context).colorScheme.primaryContainer,
|
||||||
|
child: widget.child ?? widget.closedChild,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _openDialog() {
|
||||||
|
setState(() {
|
||||||
|
_overlayEntry = OverlayEntry(
|
||||||
|
builder: (context) => CompositedTransformFollower(
|
||||||
|
offset: Offset(-_closedOffset.dx, -_closedOffset.dy),
|
||||||
|
link: _layerLink,
|
||||||
|
showWhenUnlinked: false,
|
||||||
|
child: _AnimatedOverlay(
|
||||||
|
controller: _animationController,
|
||||||
|
barrierColorAnimation: _barrierColorAnimation,
|
||||||
|
sizeAnimation: _sizeAnimation,
|
||||||
|
offsetAnimation: _offsetAnimation,
|
||||||
|
borderRadiusAnimation: _borderRadiusAnimation,
|
||||||
|
onDismiss: close,
|
||||||
|
builder: widget.closedChild != null && widget.openedChild != null
|
||||||
|
? (_) => _AnimatedSwitcher(
|
||||||
|
sizeAnimation: _sizeAnimation,
|
||||||
|
closedOpacityAnimation: _closedOpacityAnimation,
|
||||||
|
openedOpacityAnimation: _openedOpacityAnimation,
|
||||||
|
closedSize: _sizeTween.begin!,
|
||||||
|
openedSize: _sizeTween.end!,
|
||||||
|
closedChild: widget.closedChild!,
|
||||||
|
openedChild: widget.openedChild!,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
child: widget.child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
Overlay.of(context)?.insert(_overlayEntry!);
|
||||||
|
_animationController.forward();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> close() async {
|
||||||
|
_animationController.reverse();
|
||||||
|
await Future.delayed(_animationController.reverseDuration! * timeDilation);
|
||||||
|
_overlayEntry?.remove();
|
||||||
|
setState(() {
|
||||||
|
_overlayEntry = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AnimatedOverlay extends StatelessWidget {
|
||||||
|
final AnimationController controller;
|
||||||
|
final Animation<Color?> barrierColorAnimation;
|
||||||
|
final Animation<Size?> sizeAnimation;
|
||||||
|
final Animation<Size?> offsetAnimation;
|
||||||
|
final Animation<double> borderRadiusAnimation;
|
||||||
|
final VoidCallback onDismiss;
|
||||||
|
final Widget? child;
|
||||||
|
final Widget Function(BuildContext context)? builder;
|
||||||
|
|
||||||
|
const _AnimatedOverlay({
|
||||||
|
required this.controller,
|
||||||
|
required this.barrierColorAnimation,
|
||||||
|
required this.sizeAnimation,
|
||||||
|
required this.offsetAnimation,
|
||||||
|
required this.borderRadiusAnimation,
|
||||||
|
required this.onDismiss,
|
||||||
|
this.child,
|
||||||
|
this.builder,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox.fromSize(
|
||||||
|
size: MediaQuery.of(context).size,
|
||||||
|
child: AnimatedBuilder(
|
||||||
|
animation: controller,
|
||||||
|
builder: (context, _) => Stack(
|
||||||
|
children: [
|
||||||
|
Positioned.fill(
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: onDismiss,
|
||||||
|
child: ColoredBox(color: barrierColorAnimation.value!),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned.fromRect(
|
||||||
|
rect: Rect.fromCenter(
|
||||||
|
center: Offset(
|
||||||
|
offsetAnimation.value!.width,
|
||||||
|
offsetAnimation.value!.height,
|
||||||
|
),
|
||||||
|
width: sizeAnimation.value!.width,
|
||||||
|
height: sizeAnimation.value!.height,
|
||||||
|
),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(borderRadiusAnimation.value),
|
||||||
|
child: Material(
|
||||||
|
color: Theme.of(context).colorScheme.primaryContainer,
|
||||||
|
child: builder?.call(context) ?? child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AnimatedSwitcher extends StatelessWidget {
|
||||||
|
final Animation<Size?> sizeAnimation;
|
||||||
|
final Animation<double> closedOpacityAnimation;
|
||||||
|
final Animation<double> openedOpacityAnimation;
|
||||||
|
final Size closedSize;
|
||||||
|
final Size openedSize;
|
||||||
|
final Widget closedChild;
|
||||||
|
final Widget openedChild;
|
||||||
|
|
||||||
|
const _AnimatedSwitcher({
|
||||||
|
required this.sizeAnimation,
|
||||||
|
required this.closedOpacityAnimation,
|
||||||
|
required this.openedOpacityAnimation,
|
||||||
|
required this.closedSize,
|
||||||
|
required this.openedSize,
|
||||||
|
required this.closedChild,
|
||||||
|
required this.openedChild,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
Opacity(
|
||||||
|
opacity: closedOpacityAnimation.value,
|
||||||
|
child: Transform.scale(
|
||||||
|
scale: sizeAnimation.value!.width / closedSize.width,
|
||||||
|
child: SizedBox(
|
||||||
|
width: closedSize.width,
|
||||||
|
child: closedChild,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Opacity(
|
||||||
|
opacity: openedOpacityAnimation.value,
|
||||||
|
child: openedChild,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
class ReadingValue {
|
||||||
|
final String label;
|
||||||
|
final String value;
|
||||||
|
|
||||||
|
const ReadingValue({
|
||||||
|
required this.label,
|
||||||
|
required this.value,
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,20 +1,25 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:lightmeter/generated/l10n.dart';
|
import 'package:lightmeter/generated/l10n.dart';
|
||||||
import 'package:lightmeter/models/exposure_pair.dart';
|
import 'package:lightmeter/models/exposure_pair.dart';
|
||||||
|
import 'package:lightmeter/models/photography_value.dart';
|
||||||
import 'package:lightmeter/res/dimens.dart';
|
import 'package:lightmeter/res/dimens.dart';
|
||||||
|
|
||||||
|
import 'components/shared/animated_dialog.dart';
|
||||||
|
import 'components/dialog_picker.dart';
|
||||||
import 'components/reading_container.dart';
|
import 'components/reading_container.dart';
|
||||||
|
import 'models/reading_value.dart';
|
||||||
|
|
||||||
class MeteringTopBar extends StatelessWidget {
|
class MeteringTopBar extends StatelessWidget {
|
||||||
static const _columnsCount = 3;
|
static const _columnsCount = 3;
|
||||||
|
final _isoDialogKey = GlobalKey<AnimatedDialogState>();
|
||||||
|
|
||||||
final ExposurePair? fastest;
|
final ExposurePair? fastest;
|
||||||
final ExposurePair? slowest;
|
final ExposurePair? slowest;
|
||||||
final double ev;
|
final double ev;
|
||||||
final int iso;
|
final IsoValue iso;
|
||||||
final double nd;
|
final double nd;
|
||||||
|
|
||||||
const MeteringTopBar({
|
MeteringTopBar({
|
||||||
required this.fastest,
|
required this.fastest,
|
||||||
required this.slowest,
|
required this.slowest,
|
||||||
required this.ev,
|
required this.ev,
|
||||||
|
@ -38,91 +43,109 @@ class MeteringTopBar extends StatelessWidget {
|
||||||
padding: const EdgeInsets.all(Dimens.paddingM),
|
padding: const EdgeInsets.all(Dimens.paddingM),
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
bottom: false,
|
bottom: false,
|
||||||
child: Row(
|
child: MediaQuery(
|
||||||
mainAxisSize: MainAxisSize.min,
|
data: MediaQuery.of(context),
|
||||||
children: [
|
child: Row(
|
||||||
Expanded(
|
mainAxisSize: MainAxisSize.min,
|
||||||
child: Column(
|
children: [
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
Expanded(
|
||||||
children: [
|
child: Column(
|
||||||
SizedBox(
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
height: columnWidth / 3 * 4,
|
children: [
|
||||||
child: ReadingContainer(
|
SizedBox(
|
||||||
values: [
|
height: columnWidth / 3 * 4,
|
||||||
ReadingValue(
|
child: ReadingContainer(
|
||||||
label: S.of(context).fastestExposurePair,
|
values: [
|
||||||
value: fastest != null
|
ReadingValue(
|
||||||
? '${fastest!.aperture.toString()} - ${fastest!.shutterSpeed.toString()}'
|
label: S.of(context).fastestExposurePair,
|
||||||
: 'N/A',
|
value: fastest != null
|
||||||
|
? '${fastest!.aperture.toString()} - ${fastest!.shutterSpeed.toString()}'
|
||||||
|
: 'N/A',
|
||||||
|
),
|
||||||
|
ReadingValue(
|
||||||
|
label: S.of(context).slowestExposurePair,
|
||||||
|
value: fastest != null
|
||||||
|
? '${slowest!.aperture.toString()} - ${slowest!.shutterSpeed.toString()}'
|
||||||
|
: 'N/A',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const _InnerPadding(),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: columnWidth,
|
||||||
|
child: ReadingContainer.singleValue(
|
||||||
|
value: ReadingValue(
|
||||||
|
label: 'EV',
|
||||||
|
value: ev.toStringAsFixed(1),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
ReadingValue(
|
const _InnerPadding(),
|
||||||
label: S.of(context).slowestExposurePair,
|
SizedBox(
|
||||||
value: fastest != null
|
width: columnWidth,
|
||||||
? '${slowest!.aperture.toString()} - ${slowest!.shutterSpeed.toString()}'
|
child: AnimatedDialog(
|
||||||
: 'N/A',
|
key: _isoDialogKey,
|
||||||
|
closedChild: ReadingContainer.singleValue(
|
||||||
|
value: ReadingValue(
|
||||||
|
label: S.of(context).iso,
|
||||||
|
value: iso.value.toString(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
openedChild: MeteringScreenDialogPicker(
|
||||||
|
title: S.of(context).iso,
|
||||||
|
initialValue: iso,
|
||||||
|
values: isoValues,
|
||||||
|
itemTitleBuilder: (context, value) => Text(
|
||||||
|
value.value.toString(),
|
||||||
|
style: value.stopType == StopType.full
|
||||||
|
? Theme.of(context).textTheme.bodyLarge
|
||||||
|
: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
onCancel: () {
|
||||||
|
_isoDialogKey.currentState?.close();
|
||||||
|
},
|
||||||
|
onSelect: (value) {
|
||||||
|
_isoDialogKey.currentState?.close();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
)
|
||||||
),
|
],
|
||||||
const _InnerPadding(),
|
),
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
SizedBox(
|
|
||||||
width: columnWidth,
|
|
||||||
child: ReadingContainer.singleValue(
|
|
||||||
value: ReadingValue(
|
|
||||||
label: 'EV',
|
|
||||||
value: ev.toStringAsFixed(1),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const _InnerPadding(),
|
|
||||||
SizedBox(
|
|
||||||
width: columnWidth,
|
|
||||||
child: MediaQuery(
|
|
||||||
data: MediaQuery.of(context),
|
|
||||||
child: ReadingContainerWithDialog(
|
|
||||||
value: ReadingValue(
|
|
||||||
label: 'ISO',
|
|
||||||
value: iso.toString(),
|
|
||||||
),
|
|
||||||
dialogBuilder: (context) => SizedBox(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
const _InnerPadding(),
|
||||||
const _InnerPadding(),
|
SizedBox(
|
||||||
SizedBox(
|
width: columnWidth,
|
||||||
width: columnWidth,
|
child: Column(
|
||||||
child: Column(
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
children: [
|
||||||
children: [
|
AnimatedDialog(
|
||||||
ClipRRect(
|
openedSize: Size(
|
||||||
borderRadius: BorderRadius.circular(Dimens.borderRadiusM),
|
MediaQuery.of(context).size.width - Dimens.paddingM * 2,
|
||||||
child: const AspectRatio(
|
(MediaQuery.of(context).size.width - Dimens.paddingM * 2) / 3 * 4,
|
||||||
aspectRatio: 3 / 4,
|
),
|
||||||
child: ColoredBox(color: Colors.black),
|
child: const AspectRatio(
|
||||||
|
aspectRatio: 3 / 4,
|
||||||
|
child: ColoredBox(color: Colors.black),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
const _InnerPadding(),
|
||||||
const _InnerPadding(),
|
ReadingContainer.singleValue(
|
||||||
MediaQuery(
|
|
||||||
data: MediaQuery.of(context),
|
|
||||||
child: ReadingContainerWithDialog(
|
|
||||||
value: ReadingValue(
|
value: ReadingValue(
|
||||||
label: 'ND',
|
label: 'ND',
|
||||||
value: nd.toString(),
|
value: nd.toString(),
|
||||||
),
|
),
|
||||||
dialogBuilder: (context) => SizedBox(),
|
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -16,8 +16,8 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
|
||||||
|
|
||||||
MeteringBloc(this.stopType)
|
MeteringBloc(this.stopType)
|
||||||
: super(
|
: super(
|
||||||
const MeteringState(
|
MeteringState(
|
||||||
iso: 100,
|
iso: isoValues.where((element) => element.value == 100).first,
|
||||||
ev: 21.3,
|
ev: 21.3,
|
||||||
evCompensation: 0.0,
|
evCompensation: 0.0,
|
||||||
nd: 0.0,
|
nd: 0.0,
|
||||||
|
@ -38,7 +38,7 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
|
||||||
final iso = _isoValues[_random.nextInt(_isoValues.thirdStops().length)];
|
final iso = _isoValues[_random.nextInt(_isoValues.thirdStops().length)];
|
||||||
|
|
||||||
final evAtSystemIso = log2(pow(aperture.value, 2).toDouble()) - log2(shutterSpeed.value);
|
final evAtSystemIso = log2(pow(aperture.value, 2).toDouble()) - log2(shutterSpeed.value);
|
||||||
final ev = evAtSystemIso - log2(iso.value / state.iso);
|
final ev = evAtSystemIso - log2(iso.value / state.iso.value);
|
||||||
final exposurePairs = _buildExposureValues(ev);
|
final exposurePairs = _buildExposureValues(ev);
|
||||||
|
|
||||||
emit(MeteringState(
|
emit(MeteringState(
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import 'package:lightmeter/models/exposure_pair.dart';
|
import 'package:lightmeter/models/exposure_pair.dart';
|
||||||
|
import 'package:lightmeter/models/photography_value.dart';
|
||||||
|
|
||||||
class MeteringState {
|
class MeteringState {
|
||||||
final double ev;
|
final double ev;
|
||||||
final double evCompensation;
|
final double evCompensation;
|
||||||
final int iso;
|
final IsoValue iso;
|
||||||
final double nd;
|
final double nd;
|
||||||
final List<ExposurePair> exposurePairs;
|
final List<ExposurePair> exposurePairs;
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:lightmeter/generated/l10n.dart';
|
import 'package:lightmeter/generated/l10n.dart';
|
||||||
import 'package:lightmeter/res/dimens.dart';
|
import 'package:lightmeter/res/dimens.dart';
|
||||||
import 'package:lightmeter/screens/settings/settings_screen.dart';
|
|
||||||
|
|
||||||
import 'bloc_permissions_check.dart';
|
import 'bloc_permissions_check.dart';
|
||||||
import 'state_permissions_check.dart';
|
import 'state_permissions_check.dart';
|
||||||
|
|
Loading…
Reference in a new issue