mirror of
https://github.com/vodemn/m3_lightmeter.git
synced 2024-11-21 15:00: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);
|
||||
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
|
||||
"caffeine": MessageLookupByLibrary.simpleMessage("Caffeine"),
|
||||
"cancel": MessageLookupByLibrary.simpleMessage("Cancel"),
|
||||
"fastestExposurePair": MessageLookupByLibrary.simpleMessage("Fastest"),
|
||||
"haptics": MessageLookupByLibrary.simpleMessage("Haptics"),
|
||||
"iso": MessageLookupByLibrary.simpleMessage("ISO"),
|
||||
"keepsScreenOn":
|
||||
MessageLookupByLibrary.simpleMessage("Keeps screen on"),
|
||||
"openSettings": MessageLookupByLibrary.simpleMessage("Open settings"),
|
||||
|
@ -32,6 +34,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
MessageLookupByLibrary.simpleMessage("Permission needed"),
|
||||
"permissionNeededMessage": MessageLookupByLibrary.simpleMessage(
|
||||
"To use Lightmeter, turn on Camera permissions."),
|
||||
"select": MessageLookupByLibrary.simpleMessage("Select"),
|
||||
"settings": MessageLookupByLibrary.simpleMessage("Settings"),
|
||||
"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`
|
||||
String get settings {
|
||||
return Intl.message(
|
||||
|
|
|
@ -5,6 +5,9 @@
|
|||
"openSettings": "Open settings",
|
||||
"fastestExposurePair": "Fastest",
|
||||
"slowestExposurePair": "Slowest",
|
||||
"iso": "ISO",
|
||||
"cancel": "Cancel",
|
||||
"select": "Select",
|
||||
"settings": "Settings",
|
||||
"caffeine": "Caffeine",
|
||||
"keepsScreenOn": "Keeps screen on",
|
||||
|
|
|
@ -13,7 +13,6 @@ import 'res/dimens.dart';
|
|||
import 'res/theme.dart';
|
||||
import 'screens/metering/metering_bloc.dart';
|
||||
import 'screens/metering/metering_screen.dart';
|
||||
import 'screens/permissions_check/flow_permissions_check.dart';
|
||||
import 'utils/stop_type_provider.dart';
|
||||
|
||||
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/scheduler.dart';
|
||||
import 'package:lightmeter/res/dimens.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();
|
||||
});
|
||||
}
|
||||
}
|
||||
import 'package:lightmeter/screens/metering/components/topbar/models/reading_value.dart';
|
||||
|
||||
class ReadingContainer extends StatelessWidget {
|
||||
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:lightmeter/generated/l10n.dart';
|
||||
import 'package:lightmeter/models/exposure_pair.dart';
|
||||
import 'package:lightmeter/models/photography_value.dart';
|
||||
import 'package:lightmeter/res/dimens.dart';
|
||||
|
||||
import 'components/shared/animated_dialog.dart';
|
||||
import 'components/dialog_picker.dart';
|
||||
import 'components/reading_container.dart';
|
||||
import 'models/reading_value.dart';
|
||||
|
||||
class MeteringTopBar extends StatelessWidget {
|
||||
static const _columnsCount = 3;
|
||||
final _isoDialogKey = GlobalKey<AnimatedDialogState>();
|
||||
|
||||
final ExposurePair? fastest;
|
||||
final ExposurePair? slowest;
|
||||
final double ev;
|
||||
final int iso;
|
||||
final IsoValue iso;
|
||||
final double nd;
|
||||
|
||||
const MeteringTopBar({
|
||||
MeteringTopBar({
|
||||
required this.fastest,
|
||||
required this.slowest,
|
||||
required this.ev,
|
||||
|
@ -38,91 +43,109 @@ class MeteringTopBar extends StatelessWidget {
|
|||
padding: const EdgeInsets.all(Dimens.paddingM),
|
||||
child: SafeArea(
|
||||
bottom: false,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: columnWidth / 3 * 4,
|
||||
child: ReadingContainer(
|
||||
values: [
|
||||
ReadingValue(
|
||||
label: S.of(context).fastestExposurePair,
|
||||
value: fastest != null
|
||||
? '${fastest!.aperture.toString()} - ${fastest!.shutterSpeed.toString()}'
|
||||
: 'N/A',
|
||||
child: MediaQuery(
|
||||
data: MediaQuery.of(context),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: columnWidth / 3 * 4,
|
||||
child: ReadingContainer(
|
||||
values: [
|
||||
ReadingValue(
|
||||
label: S.of(context).fastestExposurePair,
|
||||
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(
|
||||
label: S.of(context).slowestExposurePair,
|
||||
value: fastest != null
|
||||
? '${slowest!.aperture.toString()} - ${slowest!.shutterSpeed.toString()}'
|
||||
: 'N/A',
|
||||
const _InnerPadding(),
|
||||
SizedBox(
|
||||
width: columnWidth,
|
||||
child: AnimatedDialog(
|
||||
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(),
|
||||
SizedBox(
|
||||
width: columnWidth,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(Dimens.borderRadiusM),
|
||||
child: const AspectRatio(
|
||||
aspectRatio: 3 / 4,
|
||||
child: ColoredBox(color: Colors.black),
|
||||
const _InnerPadding(),
|
||||
SizedBox(
|
||||
width: columnWidth,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
AnimatedDialog(
|
||||
openedSize: Size(
|
||||
MediaQuery.of(context).size.width - Dimens.paddingM * 2,
|
||||
(MediaQuery.of(context).size.width - Dimens.paddingM * 2) / 3 * 4,
|
||||
),
|
||||
child: const AspectRatio(
|
||||
aspectRatio: 3 / 4,
|
||||
child: ColoredBox(color: Colors.black),
|
||||
),
|
||||
),
|
||||
),
|
||||
const _InnerPadding(),
|
||||
MediaQuery(
|
||||
data: MediaQuery.of(context),
|
||||
child: ReadingContainerWithDialog(
|
||||
const _InnerPadding(),
|
||||
ReadingContainer.singleValue(
|
||||
value: ReadingValue(
|
||||
label: 'ND',
|
||||
value: nd.toString(),
|
||||
),
|
||||
dialogBuilder: (context) => SizedBox(),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -16,8 +16,8 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
|
|||
|
||||
MeteringBloc(this.stopType)
|
||||
: super(
|
||||
const MeteringState(
|
||||
iso: 100,
|
||||
MeteringState(
|
||||
iso: isoValues.where((element) => element.value == 100).first,
|
||||
ev: 21.3,
|
||||
evCompensation: 0.0,
|
||||
nd: 0.0,
|
||||
|
@ -38,7 +38,7 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
|
|||
final iso = _isoValues[_random.nextInt(_isoValues.thirdStops().length)];
|
||||
|
||||
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);
|
||||
|
||||
emit(MeteringState(
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import 'package:lightmeter/models/exposure_pair.dart';
|
||||
import 'package:lightmeter/models/photography_value.dart';
|
||||
|
||||
class MeteringState {
|
||||
final double ev;
|
||||
final double evCompensation;
|
||||
final int iso;
|
||||
final IsoValue iso;
|
||||
final double nd;
|
||||
final List<ExposurePair> exposurePairs;
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:lightmeter/generated/l10n.dart';
|
||||
import 'package:lightmeter/res/dimens.dart';
|
||||
import 'package:lightmeter/screens/settings/settings_screen.dart';
|
||||
|
||||
import 'bloc_permissions_check.dart';
|
||||
import 'state_permissions_check.dart';
|
||||
|
|
Loading…
Reference in a new issue