11import 'dart:ui' ;
22
33import 'package:flutter/material.dart' ;
4+ import 'package:flutter/services.dart' ;
45import 'package:provider/provider.dart' ;
56
67import '../l10n/app_localizations.dart' ;
@@ -18,12 +19,12 @@ class OnboardingScreen extends StatefulWidget {
1819
1920class _OnboardingScreenState extends State <OnboardingScreen > {
2021 static const _stepCount = 5 ;
21- static final _ages = [for (var age = 12 ; age <= 90 ; age++ ) '$age ' ];
2222
2323 final _occupationController = TextEditingController ();
2424 final _familyController = TextEditingController ();
2525 final _interestsController = TextEditingController ();
2626 final _lifeContextController = TextEditingController ();
27+ final _ageController = TextEditingController ();
2728
2829 int _step = 0 ;
2930 bool _loading = false ;
@@ -36,6 +37,7 @@ class _OnboardingScreenState extends State<OnboardingScreen> {
3637 _familyController.dispose ();
3738 _interestsController.dispose ();
3839 _lifeContextController.dispose ();
40+ _ageController.dispose ();
3941 super .dispose ();
4042 }
4143
@@ -263,31 +265,18 @@ class _OnboardingScreenState extends State<OnboardingScreen> {
263265 children: genders.map (genderButton).toList (),
264266 ),
265267 const SizedBox (height: 18 ),
266- DropdownButtonFormField <String >(
267- initialValue: _age,
268- items: _ages
269- .map (
270- (age) => DropdownMenuItem <String >(value: age, child: Text (age)),
271- )
272- .toList (),
273- onChanged: _loading
274- ? null
275- : (value) {
276- setState (() {
277- _age = value;
278- });
279- },
280- decoration: InputDecoration (
281- labelText: AppLocalizations .of (context)! .ageLabel,
282- border: OutlineInputBorder (borderRadius: BorderRadius .circular (16 )),
283- enabledBorder: OutlineInputBorder (
284- borderRadius: BorderRadius .circular (16 ),
285- ),
286- focusedBorder: OutlineInputBorder (
287- borderRadius: BorderRadius .circular (16 ),
288- ),
268+ Text (
269+ AppLocalizations .of (context)! .ageLabel,
270+ style: theme.textTheme.bodyMedium? .copyWith (
271+ color: theme.colorScheme.onSurface.withOpacity (0.7 ),
289272 ),
290273 ),
274+ const SizedBox (height: 10 ),
275+ _AgePicker (
276+ controller: _ageController,
277+ enabled: ! _loading,
278+ onChanged: (value) => setState (() => _age = value),
279+ ),
291280 ],
292281 );
293282 }
@@ -338,6 +327,7 @@ class _OnboardingScreenState extends State<OnboardingScreen> {
338327 if (_step == 0 ) {
339328 _gender = null ;
340329 _age = null ;
330+ _ageController.clear ();
341331 } else if (_step == 1 ) {
342332 _occupationController.clear ();
343333 } else if (_step == 2 ) {
@@ -415,3 +405,126 @@ class _OnboardingScreenState extends State<OnboardingScreen> {
415405 }
416406 }
417407}
408+
409+ class _AgePicker extends StatelessWidget {
410+ final TextEditingController controller;
411+ final bool enabled;
412+ final ValueChanged <String ?> onChanged;
413+
414+ const _AgePicker ({
415+ required this .controller,
416+ required this .enabled,
417+ required this .onChanged,
418+ });
419+
420+ static const int _min = 12 ;
421+ static const int _max = 90 ;
422+ static const int _default = 25 ;
423+
424+ int ? _parsed () {
425+ final raw = controller.text.trim ();
426+ if (raw.isEmpty) return null ;
427+ return int .tryParse (raw);
428+ }
429+
430+ void _bump (int delta) {
431+ final current = _parsed () ?? _default;
432+ final next = (current + delta).clamp (_min, _max);
433+ controller.text = next.toString ();
434+ controller.selection = TextSelection .collapsed (offset: controller.text.length);
435+ onChanged (controller.text);
436+ }
437+
438+ @override
439+ Widget build (BuildContext context) {
440+ final theme = Theme .of (context);
441+ final accent = theme.colorScheme.primary;
442+ return Container (
443+ padding: const EdgeInsets .symmetric (horizontal: 16 , vertical: 12 ),
444+ decoration: BoxDecoration (
445+ color: theme.colorScheme.surfaceContainerHighest.withOpacity (0.4 ),
446+ borderRadius: BorderRadius .circular (24 ),
447+ ),
448+ child: Row (
449+ children: [
450+ _RoundIconButton (
451+ icon: Icons .remove,
452+ onPressed: enabled ? () => _bump (- 1 ) : null ,
453+ accent: accent,
454+ ),
455+ Expanded (
456+ child: TextField (
457+ controller: controller,
458+ enabled: enabled,
459+ keyboardType: const TextInputType .numberWithOptions (decimal: false , signed: false ),
460+ inputFormatters: [
461+ FilteringTextInputFormatter .digitsOnly,
462+ LengthLimitingTextInputFormatter (2 ),
463+ ],
464+ textAlign: TextAlign .center,
465+ style: theme.textTheme.displaySmall? .copyWith (
466+ fontWeight: FontWeight .w600,
467+ fontFeatures: const [FontFeature .tabularFigures ()],
468+ ),
469+ decoration: const InputDecoration (
470+ hintText: '—' ,
471+ border: InputBorder .none,
472+ isDense: true ,
473+ contentPadding: EdgeInsets .symmetric (vertical: 4 ),
474+ ),
475+ onChanged: (raw) {
476+ if (raw.isEmpty) {
477+ onChanged (null );
478+ return ;
479+ }
480+ final n = int .tryParse (raw);
481+ if (n == null ) {
482+ onChanged (null );
483+ return ;
484+ }
485+ final clamped = n.clamp (_min, _max);
486+ if (clamped != n) {
487+ final s = clamped.toString ();
488+ controller.text = s;
489+ controller.selection = TextSelection .collapsed (offset: s.length);
490+ onChanged (s);
491+ } else {
492+ onChanged (raw);
493+ }
494+ },
495+ ),
496+ ),
497+ _RoundIconButton (
498+ icon: Icons .add,
499+ onPressed: enabled ? () => _bump (1 ) : null ,
500+ accent: accent,
501+ ),
502+ ],
503+ ),
504+ );
505+ }
506+ }
507+
508+ class _RoundIconButton extends StatelessWidget {
509+ final IconData icon;
510+ final VoidCallback ? onPressed;
511+ final Color accent;
512+ const _RoundIconButton ({required this .icon, required this .onPressed, required this .accent});
513+
514+ @override
515+ Widget build (BuildContext context) {
516+ return Material (
517+ color: accent.withOpacity (0.12 ),
518+ shape: const CircleBorder (),
519+ child: InkWell (
520+ customBorder: const CircleBorder (),
521+ onTap: onPressed,
522+ child: SizedBox (
523+ width: 44 ,
524+ height: 44 ,
525+ child: Icon (icon, color: accent),
526+ ),
527+ ),
528+ );
529+ }
530+ }
0 commit comments