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

@@ -13,7 +13,7 @@ class Quiz1Screen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return QuizQuestionScreen(
title: 'Quiz 1/26',
title: 'Quiz 1/25',
question: 'O rosto do seu filho/a se parece com o desta imagem?',
questionImagePaths: const ['assets/mockup_images/2.jpeg'],
suggestedYoutubeId: '', // TODO: colar o ID do YouTube do Episódio 1
@@ -52,7 +52,7 @@ class Quiz2Screen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return QuizQuestionScreen(
title: 'Quiz 2/26',
title: 'Quiz 2/25',
question:
'A boca do seu filho/a fica habitualmente na posição desta imagem (entreaberta)?',
questionImagePaths: const ['assets/mockup_images/4.jpeg'],
@@ -92,7 +92,7 @@ class Quiz3Screen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return QuizQuestionScreen(
title: 'Quiz 3/26',
title: 'Quiz 3/25',
question: 'O seu filho/a tem olheiras semelhantes às desta imagem?',
questionImagePaths: const ['assets/mockup_images/8.jpeg'],
suggestedYoutubeId: '', // TODO: colar o ID do YouTube do Episódio 3
@@ -131,7 +131,7 @@ class Quiz4Screen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return QuizQuestionScreen(
title: 'Quiz 4/26',
title: 'Quiz 4/25',
question:
'Com a boca fechada, o queixo do seu filho/a se parece com o desta imagem?',
questionImagePaths: const ['assets/mockup_images/6.jpeg'],
@@ -171,7 +171,7 @@ class Quiz5Screen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return QuizQuestionScreen(
title: 'Quiz 5/26',
title: 'Quiz 5/25',
question: 'Quantos dentes tem o seu filho/a em cima na boca?',
answers: const [],
currentScore: currentScore,
@@ -194,7 +194,7 @@ class Quiz6Screen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return QuizQuestionScreen(
title: 'Quiz 6/26',
title: 'Quiz 6/25',
question: 'Quantos dentes tem o seu filho/a em baixo na boca?',
answers: const [],
currentScore: currentScore,
@@ -217,7 +217,7 @@ class Quiz7Screen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return QuizQuestionScreen(
title: 'Quiz 7/26',
title: 'Quiz 7/25',
question: 'A boca do seu filho/a se parece com a desta imagem?',
questionImagePaths: const ['assets/mockup_images/14.jpeg'],
suggestedYoutubeId: '', // TODO: colar o ID do YouTube do Episódio 5
@@ -256,7 +256,7 @@ class Quiz8Screen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return QuizQuestionScreen(
title: 'Quiz 8/26',
title: 'Quiz 8/25',
question:
'O frénulo (freio) da língua do seu filho/a se parece com o desta imagem?',
questionImagePaths: const ['assets/mockup_images/17.png'],
@@ -296,7 +296,7 @@ class Quiz9Screen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return QuizQuestionScreen(
title: 'Quiz 9/26',
title: 'Quiz 9/25',
question: 'O seu filho/a tem problemas respiratórios diagnosticados?',
answers: const [
QuizAnswer(
@@ -332,7 +332,7 @@ class Quiz10Screen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return QuizQuestionScreen(
title: 'Quiz 10/26',
title: 'Quiz 10/25',
question: 'O seu filho/a respira habitualmente pela boca?',
answers: const [
QuizAnswer(
@@ -368,7 +368,7 @@ class Quiz11Screen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return QuizQuestionScreen(
title: 'Quiz 11/26',
title: 'Quiz 11/25',
question: 'O seu filho/a ressona habitualmente durante a noite?',
answers: const [
QuizAnswer(
@@ -404,7 +404,7 @@ class Quiz12Screen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return QuizQuestionScreen(
title: 'Quiz 12/26',
title: 'Quiz 12/25',
question: 'O seu filho/a sente habitualmente o nariz "tapado"?',
answers: const [
QuizAnswer(
@@ -440,7 +440,7 @@ class Quiz13Screen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return QuizQuestionScreen(
title: 'Quiz 13/26',
title: 'Quiz 13/25',
question:
'Durante o sono, o seu filho/a tem habitualmente interrupções da respiração?',
answers: const [
@@ -478,7 +478,7 @@ class Quiz14Screen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return QuizQuestionScreen(
title: 'Quiz 14/26',
title: 'Quiz 14/25',
question: 'O seu filho/a range os dentes com frequência?',
answers: const [
QuizAnswer(
@@ -514,7 +514,7 @@ class Quiz15Screen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return QuizQuestionScreen(
title: 'Quiz 15/26',
title: 'Quiz 15/25',
question: 'O seu filho/a habitualmente tem alergias sazonais?',
answers: const [
QuizAnswer(
@@ -550,7 +550,7 @@ class Quiz16Screen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return QuizQuestionScreen(
title: 'Quiz 16/26',
title: 'Quiz 16/25',
question: 'O seu filho/a acorda com saliva seca na cara ou na almofada?',
answers: const [
QuizAnswer(
@@ -586,7 +586,7 @@ class Quiz17Screen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return QuizQuestionScreen(
title: 'Quiz 17/26',
title: 'Quiz 17/25',
question: 'O seu filho/a teve ou costuma ter com frequência otites?',
answers: const [
QuizAnswer(
@@ -622,7 +622,7 @@ class Quiz18Screen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return QuizQuestionScreen(
title: 'Quiz 18/26',
title: 'Quiz 18/25',
question: 'O seu filho/a teve ou costuma ter com frequência amigdalites?',
answers: const [
QuizAnswer(
@@ -658,7 +658,7 @@ class Quiz19Screen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return QuizQuestionScreen(
title: 'Quiz 19/26',
title: 'Quiz 19/25',
question:
'O seu filho/a teve ou costuma ter com frequência bronquiolites?',
answers: const [
@@ -695,7 +695,7 @@ class Quiz20Screen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return QuizQuestionScreen(
title: 'Quiz 20/26',
title: 'Quiz 20/25',
question: 'O seu filho/a apresenta dificuldades a mastigar?',
answers: const [
QuizAnswer(
@@ -731,7 +731,7 @@ class Quiz21Screen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return QuizQuestionScreen(
title: 'Quiz 21/26',
title: 'Quiz 21/25',
question: 'O seu filho/a habitualmente é lento a comer?',
answers: const [
QuizAnswer(
@@ -767,7 +767,7 @@ class Quiz22Screen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return QuizQuestionScreen(
title: 'Quiz 22/26',
title: 'Quiz 22/25',
question: 'O seu filho/a habitualmente prefere comer alimentos moles?',
answers: const [
QuizAnswer(
@@ -803,7 +803,7 @@ class Quiz23Screen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return QuizQuestionScreen(
title: 'Quiz 23/26',
title: 'Quiz 23/25',
question: 'Em bebé apenas foi alimentado por biberão?',
answers: const [
QuizAnswer(
@@ -839,7 +839,7 @@ class Quiz24Screen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return QuizQuestionScreen(
title: 'Quiz 24/26',
title: 'Quiz 24/25',
question: 'O seu filho/a usa ou usou chupeta com frequência?',
answers: const [
QuizAnswer(
@@ -875,7 +875,7 @@ class Quiz25Screen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return QuizQuestionScreen(
title: 'Quiz 25/26',
title: 'Quiz 25/25',
question: 'O seu filho/a chucha ou já chuchou o dedo com frequência?',
answers: const [
QuizAnswer(
@@ -892,35 +892,6 @@ class Quiz25Screen extends StatelessWidget {
),
],
currentScore: currentScore,
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
builder: (_) => Quiz26Screen(currentScore: nextScore, scopeId: scopeId),
),
answerType: QuizAnswerType.yesNo,
showBackButton: true,
);
}
}
// Quiz 26: Final
class Quiz26Screen extends StatelessWidget {
const Quiz26Screen({super.key, required this.currentScore, this.scopeId});
final int currentScore;
final String? scopeId;
@override
Widget build(BuildContext context) {
return QuizQuestionScreen(
title: 'Quiz 26/26',
question: 'Obrigado por completar o questionário!',
answers: const [
QuizAnswer(
title: 'Concluir',
description: 'Clique para ver os resultados',
weight: 0,
),
],
currentScore: currentScore,
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
builder: (_) => QuizResultScreen(
finalScore: nextScore,
@@ -928,6 +899,7 @@ class Quiz26Screen extends StatelessWidget {
scopeId: scopeId,
),
),
answerType: QuizAnswerType.yesNo,
isFinal: true,
showBackButton: true,
);

View File

@@ -4,6 +4,8 @@ import 'package:flutter/material.dart';
import 'package:lottie/lottie.dart';
import '../screens/video_screen.dart';
import '../widgets/entrance.dart';
import '../widgets/tap_bounce.dart';
typedef QuizNextBuilder =
Route<void> Function(BuildContext context, int nextScore);
@@ -90,6 +92,10 @@ class _QuizQuestionScreenState extends State<QuizQuestionScreen> {
canProceed = _numberValue != null && _numberValue! >= 0 && !_navigating;
}
final bool hasSuggestedVideo =
(widget.suggestedVideoPath?.isNotEmpty ?? false) ||
(widget.suggestedYoutubeId?.isNotEmpty ?? false);
return Scaffold(
body: Stack(
clipBehavior: Clip.none,
@@ -127,267 +133,391 @@ class _QuizQuestionScreenState extends State<QuizQuestionScreen> {
),
),
SafeArea(
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 520),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(20, 18, 20, 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
widget.title,
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.black.withValues(alpha: 0.55),
fontWeight: FontWeight.w800,
),
),
const SizedBox(height: 6),
if (widget.questionImagePaths.isNotEmpty) ...[
const SizedBox(height: 6),
_QuestionReferenceImages(
paths: widget.questionImagePaths,
),
const SizedBox(height: 10),
],
if (widget.suggestedVideoPath != null ||
widget.suggestedYoutubeId != null) ...[
TextButton.icon(
onPressed: () => showVideoPlayerDialog(
context,
VideoData(
id: 0,
title:
widget.suggestedVideoTitle ?? 'Vídeo',
description: '',
videoPath: widget.suggestedVideoPath,
youtubeId: widget.suggestedYoutubeId,
),
),
icon: const Icon(
Icons.play_circle_outline_rounded,
color: Color(0xFF2F9E94),
),
label: Text(
widget.suggestedVideoTitle ??
'Ver vídeo (opcional)',
style: const TextStyle(
color: Color(0xFF2F9E94),
fontWeight: FontWeight.w800,
),
),
),
const SizedBox(height: 4),
],
Text(
widget.question,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w900,
color: Color(0xFFFF55A7),
height: 1.2,
),
),
const SizedBox(height: 8),
Text(
widget.answerType == QuizAnswerType.number
? 'Insira o número'
: widget.answerType == QuizAnswerType.yesNo
? 'Escolha uma opção'
: 'Escolha apenas uma opção',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.black.withValues(alpha: 0.55),
fontWeight: FontWeight.w700,
),
),
],
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(
height: 44,
child: Stack(
alignment: Alignment.center,
children: [
Text(
widget.title,
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.black.withValues(alpha: 0.55),
fontWeight: FontWeight.w800,
),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: widget.answerType == QuizAnswerType.number
? _buildNumberInput()
: ListView.separated(
padding: const EdgeInsets.only(bottom: 12),
itemCount: widget.answers.length,
separatorBuilder: (context, index) =>
const SizedBox(height: 12),
itemBuilder: (context, i) {
return _QuizAnswerTile(
answer: widget.answers[i],
selected: _selected == i,
onTap: () => setState(() => _selected = i),
);
},
if (widget.showBackButton)
Positioned(
left: 4,
child: TapBounce(
scale: 0.9,
child: Material(
color: Colors.white.withValues(alpha: 0.85),
shape: const CircleBorder(),
elevation: 4,
shadowColor: Colors.black.withValues(
alpha: 0.15,
),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(20, 8, 20, 18),
child: Column(
children: [
SizedBox(
width: size.width * 0.62,
height: 46,
child: FilledButton(
style:
FilledButton.styleFrom(
backgroundColor: const Color(0xFF2F9E94),
foregroundColor: Colors.white,
shape: const StadiumBorder(),
textStyle: const TextStyle(
fontWeight: FontWeight.w900,
),
).copyWith(
animationDuration: const Duration(
milliseconds: 180,
),
splashFactory: InkSparkle.splashFactory,
overlayColor:
WidgetStateProperty.resolveWith<Color?>(
(states) {
if (states.contains(
WidgetState.pressed,
)) {
return Colors.white.withValues(
alpha: 0.14,
);
}
if (states.contains(
WidgetState.hovered,
) ||
states.contains(
WidgetState.focused,
)) {
return Colors.white.withValues(
alpha: 0.08,
);
}
return null;
},
),
child: InkWell(
customBorder: const CircleBorder(),
onTap: () => Navigator.of(context).maybePop(),
child: const Padding(
padding: EdgeInsets.all(10),
child: Icon(
Icons.arrow_back_rounded,
color: Color(0xFF2F9E94),
size: 22,
),
onPressed: !canProceed
? null
: () {
setState(() => _navigating = true);
int nextScore = widget.currentScore;
if (widget.answerType ==
QuizAnswerType.number) {
nextScore =
widget.currentScore +
(_numberValue ?? 0);
} else {
final picked =
widget.answers[_selected!];
nextScore =
widget.currentScore + picked.weight;
}
if (widget.isFinal) {
final finishedRoute = widget.nextRoute(
context,
nextScore,
);
Navigator.of(
context,
).pushReplacement(finishedRoute);
return;
}
Navigator.of(context).push(
widget.nextRoute(context, nextScore),
);
},
child: Text(
widget.isFinal ? 'Concluir' : 'Avançar',
),
),
),
),
if (widget.showBackButton) ...[
const SizedBox(height: 10),
SizedBox(
width: size.width * 0.62,
height: 42,
child: FilledButton(
style:
FilledButton.styleFrom(
backgroundColor: const Color(0xFF2F9E94),
foregroundColor: Colors.white,
shape: const StadiumBorder(),
textStyle: const TextStyle(
fontWeight: FontWeight.w900,
),
).copyWith(
animationDuration: const Duration(
milliseconds: 180,
),
splashFactory: InkSparkle.splashFactory,
overlayColor:
WidgetStateProperty.resolveWith<
Color?
>((states) {
if (states.contains(
WidgetState.pressed,
)) {
return Colors.white.withValues(
alpha: 0.14,
);
}
if (states.contains(
WidgetState.hovered,
) ||
states.contains(
WidgetState.focused,
)) {
return Colors.white.withValues(
alpha: 0.08,
);
}
return null;
}),
),
onPressed: () =>
Navigator.of(context).maybePop(),
child: const Text('Voltar'),
),
),
],
const SizedBox(height: 10),
SizedBox(
width: size.width * 0.62,
height: 42,
child: OutlinedButton(
style: OutlinedButton.styleFrom(
foregroundColor: const Color(0xFF2F9E94),
side: const BorderSide(
color: Color(0xFF2F9E94),
width: 1.3,
),
shape: const StadiumBorder(),
textStyle: const TextStyle(
fontWeight: FontWeight.w900,
),
),
onPressed: () => Navigator.of(
context,
).popUntil((route) => route.isFirst),
child: const Text('Voltar para homepage'),
),
),
],
),
),
],
),
],
),
),
),
Expanded(
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 520,
),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 16,
),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
FadeSlideIn(
child: Padding(
padding: const EdgeInsets.fromLTRB(
20,
4,
20,
10,
),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.stretch,
children: [
if (widget
.questionImagePaths
.isNotEmpty) ...[
const SizedBox(height: 6),
_QuestionReferenceImages(
paths:
widget.questionImagePaths,
),
const SizedBox(height: 10),
],
if (hasSuggestedVideo) ...[
TextButton.icon(
onPressed: () =>
showVideoPlayerDialog(
context,
VideoData(
id: 0,
title:
widget
.suggestedVideoTitle ??
'Vídeo',
description: '',
videoPath: widget
.suggestedVideoPath,
youtubeId: widget
.suggestedYoutubeId,
),
),
icon: const Icon(
Icons
.play_circle_outline_rounded,
color: Color(0xFF2F9E94),
),
label: Text(
widget.suggestedVideoTitle ??
'Ver vídeo (opcional)',
style: const TextStyle(
color: Color(0xFF2F9E94),
fontWeight:
FontWeight.w800,
),
),
),
const SizedBox(height: 4),
],
Text(
widget.question,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w900,
color: Color(0xFFFF55A7),
height: 1.2,
),
),
const SizedBox(height: 8),
Text(
widget.answerType ==
QuizAnswerType.number
? 'Insira o número'
: widget.answerType ==
QuizAnswerType.yesNo
? 'Escolha uma opção'
: 'Escolha apenas uma opção',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.black
.withValues(alpha: 0.55),
fontWeight: FontWeight.w700,
),
),
],
),
),
),
const SizedBox(height: 18),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
),
child:
widget.answerType ==
QuizAnswerType.number
? _buildNumberInput()
: Column(
crossAxisAlignment:
CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
for (
int i = 0;
i < widget.answers.length;
i++
) ...[
if (i > 0)
const SizedBox(height: 12),
FadeSlideIn(
delay: Duration(
milliseconds: 60 * i,
),
child: _QuizAnswerTile(
answer:
widget.answers[i],
selected:
_selected == i,
onTap: () => setState(
() => _selected = i,
),
),
),
],
],
),
),
const SizedBox(height: 24),
Padding(
padding: const EdgeInsets.fromLTRB(
20,
0,
20,
0,
),
child: Column(
children: [
TapBounce(
child: SizedBox(
width: size.width * 0.62,
height: 46,
child: FilledButton(
style:
FilledButton.styleFrom(
backgroundColor:
const Color(
0xFF2F9E94,
),
foregroundColor:
Colors.white,
shape:
const StadiumBorder(),
textStyle:
const TextStyle(
fontWeight:
FontWeight
.w900,
),
).copyWith(
animationDuration:
const Duration(
milliseconds: 180,
),
splashFactory: InkSparkle
.splashFactory,
overlayColor:
WidgetStateProperty.resolveWith<
Color?
>((states) {
if (states
.contains(
WidgetState
.pressed,
)) {
return Colors
.white
.withValues(
alpha:
0.14,
);
}
if (states.contains(
WidgetState
.hovered,
) ||
states.contains(
WidgetState
.focused,
)) {
return Colors
.white
.withValues(
alpha:
0.08,
);
}
return null;
}),
),
onPressed: !canProceed
? null
: () async {
setState(
() => _navigating =
true,
);
int nextScore =
widget
.currentScore;
if (widget.answerType ==
QuizAnswerType
.number) {
nextScore =
widget
.currentScore +
(_numberValue ??
0);
} else {
final picked = widget
.answers[_selected!];
nextScore =
widget
.currentScore +
picked.weight;
}
if (widget.isFinal) {
final finishedRoute =
widget.nextRoute(
context,
nextScore,
);
Navigator.of(
context,
).pushReplacement(
finishedRoute,
);
return;
}
await Navigator.of(
context,
).push(
widget.nextRoute(
context,
nextScore,
),
);
if (mounted) {
setState(
() => _navigating =
false,
);
}
},
child: Text(
widget.isFinal
? 'Concluir'
: 'Avançar',
),
),
),
),
const SizedBox(height: 10),
TapBounce(
child: SizedBox(
width: size.width * 0.62,
height: 42,
child: OutlinedButton(
style:
OutlinedButton.styleFrom(
foregroundColor:
const Color(
0xFF2F9E94,
),
side: const BorderSide(
color: Color(
0xFF2F9E94,
),
width: 1.3,
),
shape:
const StadiumBorder(),
textStyle:
const TextStyle(
fontWeight:
FontWeight
.w900,
),
),
onPressed: () =>
Navigator.of(
context,
).popUntil(
(route) =>
route.isFirst,
),
child: const Text(
'Voltar para homepage',
),
),
),
),
],
),
),
],
),
),
),
),
),
);
},
),
),
],
),
),
],
@@ -469,67 +599,109 @@ class _QuizAnswerTile extends StatelessWidget {
? Colors.white.withValues(alpha: 0.88)
: Colors.white.withValues(alpha: 0.70);
return AnimatedContainer(
duration: const Duration(milliseconds: 220),
curve: Curves.easeOutCubic,
decoration: BoxDecoration(
color: bg,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: borderColor, width: selected ? 1.4 : 1.0),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.06),
blurRadius: 18,
offset: const Offset(0, 10),
),
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(16),
onTap: onTap,
splashFactory: InkSparkle.splashFactory,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (answer.imagePath != null) ...[
ClipRRect(
borderRadius: BorderRadius.circular(12),
child: AspectRatio(
aspectRatio: 4 / 3,
child: Image.asset(
answer.imagePath!,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) => Container(
color: Colors.black.withValues(alpha: 0.06),
child: const Center(
child: Icon(
Icons.image_not_supported_outlined,
color: Colors.black38,
),
),
),
),
),
),
const SizedBox(height: 10),
],
Text(
answer.title,
textAlign: TextAlign.center,
style: const TextStyle(
fontWeight: FontWeight.w900,
fontSize: 15,
color: Color(0xFF2F9E94),
),
return TapBounce(
scale: 0.97,
child: Stack(
clipBehavior: Clip.none,
fit: StackFit.passthrough,
children: [
AnimatedContainer(
duration: const Duration(milliseconds: 220),
curve: Curves.easeOutCubic,
decoration: BoxDecoration(
color: bg,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: borderColor,
width: selected ? 1.4 : 1.0,
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.06),
blurRadius: 18,
offset: const Offset(0, 10),
),
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(16),
onTap: onTap,
splashFactory: InkSparkle.splashFactory,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 14,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (answer.imagePath != null) ...[
ClipRRect(
borderRadius: BorderRadius.circular(12),
child: AspectRatio(
aspectRatio: 4 / 3,
child: Image.asset(
answer.imagePath!,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) =>
Container(
color: Colors.black.withValues(
alpha: 0.06,
),
child: const Center(
child: Icon(
Icons.image_not_supported_outlined,
color: Colors.black38,
),
),
),
),
),
),
const SizedBox(height: 10),
],
Text(
answer.title,
textAlign: TextAlign.center,
style: const TextStyle(
fontWeight: FontWeight.w900,
fontSize: 15,
color: Color(0xFF2F9E94),
),
),
],
),
),
),
),
),
),
Positioned(
top: 8,
right: 8,
child: IgnorePointer(
child: AnimatedScale(
scale: selected ? 1.0 : 0.0,
duration: const Duration(milliseconds: 220),
curve: Curves.easeOutBack,
child: Container(
width: 22,
height: 22,
decoration: const BoxDecoration(
color: Color(0xFF2F9E94),
shape: BoxShape.circle,
),
child: const Icon(
Icons.check_rounded,
size: 15,
color: Colors.white,
),
),
),
),
),
],
),
);
}

View File

@@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
import 'dart:async';
import '../main.dart' show supabase;
import '../widgets/entrance.dart';
import '../widgets/tap_bounce.dart';
import 'quiz_prefs.dart';
class QuizResultScreen extends StatefulWidget {
@@ -115,82 +117,98 @@ class _QuizResultScreenState extends State<QuizResultScreen> {
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 6),
const Text(
'A percentagem de risco\navaliada é de:',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w900,
color: Color(0xFFFF55A7),
height: 1.2,
),
),
const SizedBox(height: 18),
Center(
child: SizedBox(
width: 220,
height: 220,
child: Stack(
alignment: Alignment.center,
children: [
SizedBox(
width: 200,
height: 200,
child: CircularProgressIndicator(
value: progress,
strokeWidth: 12,
backgroundColor: Colors.black
.withValues(alpha: 0.10),
valueColor:
const AlwaysStoppedAnimation(
Color(0xFF2F9E94),
),
),
),
Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'$percent%',
style: const TextStyle(
fontSize: 34,
fontWeight: FontWeight.w900,
color: Colors.black,
),
),
const SizedBox(height: 4),
Text(
'${clamped.toInt()}/${widget.maxScore}',
style: TextStyle(
color: Colors.black.withValues(
alpha: 0.60,
),
fontWeight: FontWeight.w800,
),
),
],
),
],
FadeSlideIn(
child: const Text(
'A percentagem de risco\navaliada é de:',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w900,
color: Color(0xFFFF55A7),
height: 1.2,
),
),
),
const SizedBox(height: 18),
Text(
'Conclusões:',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.black.withValues(alpha: 0.75),
fontWeight: FontWeight.w900,
Center(
child: TweenAnimationBuilder<double>(
duration: const Duration(milliseconds: 1100),
curve: Curves.easeOutCubic,
tween: Tween<double>(begin: 0, end: progress),
builder: (context, animatedProgress, _) {
final animatedPercent =
(animatedProgress * 100).round();
return SizedBox(
width: 220,
height: 220,
child: Stack(
alignment: Alignment.center,
children: [
SizedBox(
width: 200,
height: 200,
child: CircularProgressIndicator(
value: animatedProgress,
strokeWidth: 12,
backgroundColor: Colors.black
.withValues(alpha: 0.10),
valueColor:
const AlwaysStoppedAnimation(
Color(0xFF2F9E94),
),
),
),
Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'$animatedPercent%',
style: const TextStyle(
fontSize: 34,
fontWeight: FontWeight.w900,
color: Colors.black,
),
),
const SizedBox(height: 4),
Text(
'${clamped.toInt()}/${widget.maxScore}',
style: TextStyle(
color: Colors.black
.withValues(alpha: 0.60),
fontWeight: FontWeight.w800,
),
),
],
),
],
),
);
},
),
),
const SizedBox(height: 18),
FadeSlideIn(
delay: const Duration(milliseconds: 120),
child: Text(
'Conclusões:',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.black.withValues(alpha: 0.75),
fontWeight: FontWeight.w900,
),
),
),
const SizedBox(height: 10),
Text(
'Esta avaliação é apenas educativa.\nSe tiver dúvidas ou sinais de cárie/dor, procure um Dentista.',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.black.withValues(alpha: 0.70),
fontWeight: FontWeight.w600,
height: 1.25,
FadeSlideIn(
delay: const Duration(milliseconds: 160),
child: Text(
'Esta avaliação é apenas educativa.\nSe tiver dúvidas ou sinais de cárie/dor, procure um Dentista.',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.black.withValues(alpha: 0.70),
fontWeight: FontWeight.w600,
height: 1.25,
),
),
),
const SizedBox(height: 16),
@@ -210,7 +228,8 @@ class _QuizResultScreenState extends State<QuizResultScreen> {
),
),
Center(
child: SizedBox(
child: TapBounce(
child: SizedBox(
width: 260,
height: 46,
child: FilledButton(
@@ -229,6 +248,7 @@ class _QuizResultScreenState extends State<QuizResultScreen> {
},
child: const Text('Avançar'),
),
),
),
),
],