Nova tela de login | Adaptaçao nova da AppBar | Animções novas
This commit is contained in:
62
lib/widgets/animated_nav_icon.dart
Normal file
62
lib/widgets/animated_nav_icon.dart
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
64
lib/widgets/entrance.dart
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
60
lib/widgets/tap_bounce.dart
Normal file
60
lib/widgets/tap_bounce.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user