Implemented metering dialogs animation

added readings dialog animation (wip)

added offset animation

added size animation

added backgroundColor animation

added borderRadius animation

added opacity animations

fixed closing dialog

added mock dialog for ND
This commit is contained in:
Vadim 2022-11-29 21:46:23 +03:00
parent 8697bcfec7
commit 7c20bfe80d
4 changed files with 216 additions and 9 deletions

View file

@ -79,7 +79,7 @@ class _ApplicationState extends State<Application> with TickerProviderStateMixin
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: S.delegate.supportedLocales,
home: const PermissionsCheckFlow(),
home: MeteringScreen(animationController: _animationController),
routes: {
"metering": (context) => MeteringScreen(animationController: _animationController),
"settings": (context) => const SettingsScreen(),

View file

@ -3,6 +3,7 @@
class Dimens {
static const double borderRadiusM = 16;
static const double borderRadiusL = 24;
static const double borderRadiusXL = 32;
static const double grid4 = 4;
static const double grid8 = 8;
@ -16,5 +17,6 @@ class Dimens {
static const Duration durationS = Duration(milliseconds: 100);
static const Duration durationSM = Duration(milliseconds: 150);
static const Duration durationM = Duration(milliseconds: 200);
static const Duration durationML = Duration(milliseconds: 250);
static const Duration durationL = Duration(milliseconds: 300);
}

View file

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:lightmeter/res/dimens.dart';
class ReadingValue {
@ -11,6 +12,202 @@ class ReadingValue {
});
}
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 {
final List<_ReadingValueBuilder> _items;

View file

@ -79,10 +79,14 @@ class MeteringTopBar extends StatelessWidget {
const _InnerPadding(),
SizedBox(
width: columnWidth,
child: ReadingContainer.singleValue(
value: ReadingValue(
label: 'ISO',
value: iso.toString(),
child: MediaQuery(
data: MediaQuery.of(context),
child: ReadingContainerWithDialog(
value: ReadingValue(
label: 'ISO',
value: iso.toString(),
),
dialogBuilder: (context) => SizedBox(),
),
),
),
@ -105,10 +109,14 @@ class MeteringTopBar extends StatelessWidget {
),
),
const _InnerPadding(),
ReadingContainer.singleValue(
value: ReadingValue(
label: 'ND',
value: nd.toString(),
MediaQuery(
data: MediaQuery.of(context),
child: ReadingContainerWithDialog(
value: ReadingValue(
label: 'ND',
value: nd.toString(),
),
dialogBuilder: (context) => SizedBox(),
),
),
],