Nova tela de login | Adaptaçao nova da AppBar | Animções novas

This commit is contained in:
Carlos Correia
2026-07-07 21:40:05 +01:00
parent a8e04ceeb2
commit 2ced93afdd
15 changed files with 1680 additions and 1322 deletions

View File

@@ -0,0 +1,62 @@
import 'package:flutter/material.dart';
/// Ícone de navegação que dá um pequeno "pulo" (scale bounce) sempre que
/// passa a ficar selecionado, para reforçar o feedback de toque na
/// bottom navigation bar.
class AnimatedNavIcon extends StatefulWidget {
const AnimatedNavIcon({
super.key,
required this.icon,
required this.selected,
});
final IconData icon;
final bool selected;
@override
State<AnimatedNavIcon> createState() => _AnimatedNavIconState();
}
class _AnimatedNavIconState extends State<AnimatedNavIcon>
with SingleTickerProviderStateMixin {
late final AnimationController _controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 320),
);
late final Animation<double> _bounce = TweenSequence<double>([
TweenSequenceItem(
tween: Tween(begin: 1.0, end: 1.35).chain(
CurveTween(curve: Curves.easeOut),
),
weight: 40,
),
TweenSequenceItem(
tween: Tween(begin: 1.35, end: 1.0).chain(
CurveTween(curve: Curves.easeOutBack),
),
weight: 60,
),
]).animate(_controller);
@override
void didUpdateWidget(covariant AnimatedNavIcon oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.selected && !oldWidget.selected) {
_controller.forward(from: 0);
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ScaleTransition(
scale: _bounce,
child: Icon(widget.icon),
);
}
}

View File

@@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
import 'tap_bounce.dart';
const Color _teal = Color(0xFF2F9E94);
const Color _accentPink = Color(0xFFFF55A7);
@@ -24,20 +26,24 @@ Future<bool?> showConfirmDialog(
),
content: message == null ? null : Text(message),
actions: [
TextButton(
style: TextButton.styleFrom(foregroundColor: _teal),
onPressed: () => Navigator.of(ctx).pop(false),
child: Text(cancelLabel),
),
FilledButton(
style: FilledButton.styleFrom(
backgroundColor: confirmColor,
foregroundColor: Colors.white,
shape: const StadiumBorder(),
textStyle: const TextStyle(fontWeight: FontWeight.w800),
TapBounce(
child: TextButton(
style: TextButton.styleFrom(foregroundColor: _teal),
onPressed: () => Navigator.of(ctx).pop(false),
child: Text(cancelLabel),
),
),
TapBounce(
child: FilledButton(
style: FilledButton.styleFrom(
backgroundColor: confirmColor,
foregroundColor: Colors.white,
shape: const StadiumBorder(),
textStyle: const TextStyle(fontWeight: FontWeight.w800),
),
onPressed: () => Navigator.of(ctx).pop(true),
child: Text(confirmLabel),
),
onPressed: () => Navigator.of(ctx).pop(true),
child: Text(confirmLabel),
),
],
);

64
lib/widgets/entrance.dart Normal file
View File

@@ -0,0 +1,64 @@
import 'package:flutter/material.dart';
/// Animação de entrada (fade + leve deslize para cima) para dar vida a
/// cards e listas quando aparecem em ecrã. Suporta [delay] para permitir
/// efeito "staggered" (itens surgindo em sequência) em listas/grades.
class FadeSlideIn extends StatefulWidget {
const FadeSlideIn({
super.key,
required this.child,
this.delay = Duration.zero,
this.duration = const Duration(milliseconds: 420),
this.offset = const Offset(0, 0.08),
});
final Widget child;
final Duration delay;
final Duration duration;
final Offset offset;
@override
State<FadeSlideIn> createState() => _FadeSlideInState();
}
class _FadeSlideInState extends State<FadeSlideIn>
with SingleTickerProviderStateMixin {
late final AnimationController _controller = AnimationController(
vsync: this,
duration: widget.duration,
);
late final Animation<double> _fade = CurvedAnimation(
parent: _controller,
curve: Curves.easeOut,
);
late final Animation<Offset> _slide = Tween<Offset>(
begin: widget.offset,
end: Offset.zero,
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic));
@override
void initState() {
super.initState();
if (widget.delay == Duration.zero) {
_controller.forward();
} else {
Future.delayed(widget.delay, () {
if (mounted) _controller.forward();
});
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return FadeTransition(
opacity: _fade,
child: SlideTransition(position: _slide, child: widget.child),
);
}
}

View File

@@ -0,0 +1,60 @@
import 'package:flutter/material.dart';
/// Envolve [child] com um efeito de "aperto" ao toque: encolhe levemente
/// no pointer-down e volta ao tamanho normal com uma pequena mola ao soltar.
///
/// Usa [Listener] (eventos de ponteiro puros) em vez de [GestureDetector]
/// para não competir na arena de gestos com um `InkWell`/`Button` filho —
/// o toque real continua a ser tratado pelo widget interno normalmente.
class TapBounce extends StatefulWidget {
const TapBounce({
super.key,
required this.child,
this.scale = 0.94,
this.duration = const Duration(milliseconds: 110),
});
final Widget child;
final double scale;
final Duration duration;
@override
State<TapBounce> createState() => _TapBounceState();
}
class _TapBounceState extends State<TapBounce>
with SingleTickerProviderStateMixin {
late final AnimationController _controller = AnimationController(
vsync: this,
duration: widget.duration,
);
late final Animation<double> _scale = Tween<double>(
begin: 1.0,
end: widget.scale,
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut));
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _press(PointerDownEvent _) => _controller.forward();
void _release([PointerEvent? _]) => _controller.reverse();
@override
Widget build(BuildContext context) {
return Listener(
onPointerDown: _press,
onPointerUp: _release,
onPointerCancel: _release,
child: AnimatedBuilder(
animation: _scale,
builder: (context, child) =>
Transform.scale(scale: _scale.value, child: child),
child: widget.child,
),
);
}
}