From f16df261feb703b20147c4ba406ed1fa3450348b Mon Sep 17 00:00:00 2001 From: Luke Walton Date: Fri, 1 Mar 2024 16:01:50 +0000 Subject: [PATCH] feat: Dial Pad (#30) --- .github/CODEOWNERS | 1 + example/lib/home.dart | 19 +- .../lib/pages/components/dialpad_example.dart | 95 ++++++++ .../pages/components/progress_example.dart | 24 +- example/test/dialpad_test.dart | 116 +++++++++ example/web/index.html | 17 +- .../pages/components/dial_pad_widgetbook.dart | 45 ++++ example/widgetbook/widgetbook.dart | 2 + lib/src/components/dial_pad/dial_pad.dart | 220 ++++++++++++++++++ lib/src/components/progress/progress.dart | 7 +- lib/src/components/progress/progress_bar.dart | 122 +++++----- lib/src/theme/typography.dart | 2 +- lib/src/utils/utils.dart | 37 +++ lib/zeta_flutter.dart | 1 + 14 files changed, 606 insertions(+), 102 deletions(-) create mode 100644 .github/CODEOWNERS create mode 100644 example/lib/pages/components/dialpad_example.dart create mode 100644 example/test/dialpad_test.dart create mode 100644 example/widgetbook/pages/components/dial_pad_widgetbook.dart create mode 100644 lib/src/components/dial_pad/dial_pad.dart create mode 100644 lib/src/utils/utils.dart diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..134cce95 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @benken @mikecoomber @thelukewalton \ No newline at end of file diff --git a/example/lib/home.dart b/example/lib/home.dart index 19b41578..d79a1916 100644 --- a/example/lib/home.dart +++ b/example/lib/home.dart @@ -8,6 +8,7 @@ import 'package:zeta_example/pages/components/bottom_sheet_example.dart'; import 'package:zeta_example/pages/components/button_example.dart'; import 'package:zeta_example/pages/components/checkbox_example.dart'; import 'package:zeta_example/pages/components/chip_example.dart'; +import 'package:zeta_example/pages/components/dialpad_example.dart'; import 'package:zeta_example/pages/theme/color_example.dart'; import 'package:zeta_example/pages/components/password_input_example.dart'; import 'package:zeta_example/pages/components/progress_example.dart'; @@ -32,9 +33,9 @@ final List components = [ Component(ButtonExample.name, (context) => const ButtonExample()), Component(CheckBoxExample.name, (context) => const CheckBoxExample()), Component(ChipExample.name, (context) => const ChipExample()), - Component( - PasswordInputExample.name, (context) => const PasswordInputExample()), - Component(ProgressExample.name, (context) => const ProgressExample()) + Component(PasswordInputExample.name, (context) => const PasswordInputExample()), + Component(ProgressExample.name, (context) => const ProgressExample()), + Component(DialPadExample.name, (context) => const DialPadExample()), ]; final List theme = [ @@ -96,27 +97,21 @@ class _HomeState extends State { title: Text('Widgets'), backgroundColor: Zeta.of(context).colors.warm.shade30, children: _components - .map((item) => ListTile( - title: Text(item.name), - onTap: () => context.go('/${item.name}'))) + .map((item) => ListTile(title: Text(item.name), onTap: () => context.go('/${item.name}'))) .toList(), ), ExpansionTile( title: Text('Theme'), backgroundColor: Zeta.of(context).colors.warm.shade30, children: _theme - .map((item) => ListTile( - title: Text(item.name), - onTap: () => context.go('/${item.name}'))) + .map((item) => ListTile(title: Text(item.name), onTap: () => context.go('/${item.name}'))) .toList(), ), ExpansionTile( title: Text('Assets'), backgroundColor: Zeta.of(context).colors.warm.shade30, children: _assets - .map((item) => ListTile( - title: Text(item.name), - onTap: () => context.go('/${item.name}'))) + .map((item) => ListTile(title: Text(item.name), onTap: () => context.go('/${item.name}'))) .toList(), ), ], diff --git a/example/lib/pages/components/dialpad_example.dart b/example/lib/pages/components/dialpad_example.dart new file mode 100644 index 00000000..91f6583a --- /dev/null +++ b/example/lib/pages/components/dialpad_example.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; +import 'package:zeta_example/widgets.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +const double _paddingSize = 40; + +class DialPadExample extends StatefulWidget { + static const String name = 'DialPad'; + + const DialPadExample({super.key}); + + @override + State createState() => _DialPadExampleState(); +} + +class _DialPadExampleState extends State { + String number = '', text = ''; + + @override + Widget build(BuildContext context) { + return ExampleScaffold( + name: DialPadExample.name, + child: LayoutBuilder(builder: (context, constraints) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: constraints.maxWidth, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const SizedBox(width: _paddingSize), + SizedBox( + width: constraints.maxWidth - (_paddingSize * 2), + child: Text( + number, + style: ZetaTextStyles.heading3, + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + ), + ), + IconButton( + icon: Icon(Icons.backspace), + onPressed: () => number.length == 0 + ? null + : setState( + () => number = number.substring(0, (number.length - 1)), + ), + ) + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const SizedBox(), + Text( + text, + style: ZetaTextStyles.heading3, + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + ), + IconButton( + icon: Icon(Icons.backspace), + onPressed: () => text.length == 0 + ? null + : setState( + () => text = text.substring(0, text.length - 1), + ), + ) + ], + ), + ZetaDialPad( + onNumber: (value) => setState(() => number += value), + onText: (value) => setState(() => text += value), + ), + ZetaButton.primary( + label: 'Clear', + borderType: ZetaWidgetBorder.full, + onPressed: () => setState(() => number = text = ''), + ) + ].divide(const SizedBox(height: ZetaSpacing.m)).toList(), + ), + ), + ], + ); + }), + ); + } +} diff --git a/example/lib/pages/components/progress_example.dart b/example/lib/pages/components/progress_example.dart index 84167758..2158b5ee 100644 --- a/example/lib/pages/components/progress_example.dart +++ b/example/lib/pages/components/progress_example.dart @@ -28,11 +28,7 @@ class ProgressExampleState extends State { SizedBox( height: 20, ), - Wrapper( - stepsCompleted: 0, - type: ZetaBarType.standard, - isThin: false, - stateChangeable: true), + Wrapper(stepsCompleted: 0, type: ZetaBarType.standard, isThin: false, stateChangeable: true), SizedBox( height: 20, ), @@ -94,9 +90,7 @@ class _WrapperState extends State { void setLoading() { setState(() { - type = type == ZetaBarType.buffering - ? ZetaBarType.standard - : ZetaBarType.buffering; + type = type == ZetaBarType.buffering ? ZetaBarType.standard : ZetaBarType.buffering; }); } @@ -108,25 +102,17 @@ class _WrapperState extends State { SizedBox( width: 400, child: ZetaProgressBar( - progress: progress, - rounded: widget.rounded, - type: type, - isThin: widget.isThin, - label: widget.label), + progress: progress, rounded: widget.rounded, type: type, isThin: widget.isThin, label: widget.label), ), const SizedBox(width: 40), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ widget.type != ZetaBarType.indeterminate - ? FilledButton( - onPressed: increasePercentage, child: Text("Increase")) + ? FilledButton(onPressed: increasePercentage, child: Text("Increase")) : Container(), const SizedBox(width: 40), - widget.stateChangeable - ? FilledButton( - onPressed: setLoading, child: Text("Start Buffering")) - : Container() + widget.stateChangeable ? FilledButton(onPressed: setLoading, child: Text("Start Buffering")) : Container() ], ) ], diff --git a/example/test/dialpad_test.dart b/example/test/dialpad_test.dart new file mode 100644 index 00000000..e0d5869e --- /dev/null +++ b/example/test/dialpad_test.dart @@ -0,0 +1,116 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +import 'test_components.dart'; + +void main() { + group('ZetaDialPad Tests', () { + testWidgets('Initializes with correct parameters', (WidgetTester tester) async { + String number = '', text = ''; + + Future debounceWait() => tester.binding.delayed(const Duration(milliseconds: 500)); + + await tester.pumpWidget( + TestWidget( + screenSize: Size(1000, 1000), + widget: ZetaDialPad( + onNumber: (value) => number += value, + onText: (value) => text += value, + ), + ), + ); + final dialPadFinder = find.byType(ZetaDialPad); + final buttonFinder = find.byType(ZetaDialPadButton); + + final oneFinder = find.byWidgetPredicate((widget) => widget is ZetaDialPadButton && widget.primary == '1'); + final twoFinder = find.byWidgetPredicate((widget) => widget is ZetaDialPadButton && widget.primary == '2'); + final threeFinder = find.byWidgetPredicate((widget) => widget is ZetaDialPadButton && widget.primary == '3'); + final starFinder = find.byWidgetPredicate((widget) => widget is ZetaDialPadButton && widget.primary == '*'); + final hashFinder = find.byWidgetPredicate((widget) => widget is ZetaDialPadButton && widget.primary == '#'); + + final ZetaDialPad dialPad = tester.firstWidget(dialPadFinder); + final List dialPadButtons = tester.widgetList(buttonFinder).toList(); + + final ZetaDialPadButton one = tester.firstWidget(oneFinder); + final ZetaDialPadButton two = tester.firstWidget(twoFinder); + final ZetaDialPadButton three = tester.firstWidget(threeFinder); + + /// Dial Pad built correctly. + expect(dialPad.buttonsPerRow, 3); + expect(dialPadButtons.length, 12); + + /// Dial Pad buttons built correctly. + expect(one.primary, '1'); + expect(one.secondary, ''); + expect(two.primary, '2'); + expect(two.secondary, 'ABC'); + expect(three.primary, '3'); + expect(three.secondary, 'DEF'); + + /// Tap button with only number. + await tester.tap(oneFinder); + await tester.pump(); + expect(number, '1'); + + /// Tap button with number and text. + await tester.tap(twoFinder); + await tester.pump(); + await debounceWait(); + expect(number, '12'); + expect(text, 'A'); + + /// Tap different button. + await tester.tap(threeFinder); + await tester.pump(); + await debounceWait(); + expect(number, '123'); + expect(text, 'AD'); + + /// Tap text button twice. + await tester.tap(twoFinder); + await tester.tap(twoFinder); + await tester.pump(); + await debounceWait(); + expect(text, 'ADB'); + + /// Tap text button thrice. + await tester.tap(twoFinder); + await tester.tap(twoFinder); + await tester.tap(twoFinder); + await tester.pump(); + await debounceWait(); + expect(text, 'ADBC'); + + /// Tap different text buttons to ensure debounce is cancelled. + await tester.tap(twoFinder); + await tester.tap(threeFinder); + await tester.tap(twoFinder); + await tester.pump(); + await debounceWait(); + expect(text, 'ADBCADA'); + + /// Tap text button more times than there is options to ensure it loops around correctly. + await tester.tap(threeFinder); + await tester.tap(threeFinder); + await tester.tap(threeFinder); + await tester.tap(threeFinder); + await tester.tap(threeFinder); + await tester.tap(threeFinder); + await tester.tap(oneFinder); + await tester.pump(); + expect(text, 'ADBCADAF'); + number = ''; + + /// Tap buttons with symbols + await tester.ensureVisible(starFinder); + await tester.tap(starFinder); + await tester.tap(hashFinder); + await tester.pump(); + expect(number, '*#'); + + /// Allow all timers to end in text debounce + await debounceWait(); + }); + }); +} diff --git a/example/web/index.html b/example/web/index.html index 115cc54e..f9b1d73c 100644 --- a/example/web/index.html +++ b/example/web/index.html @@ -1,5 +1,6 @@ + @@ -27,28 +28,29 @@ - + zeta_flutter_example + - + + \ No newline at end of file diff --git a/example/widgetbook/pages/components/dial_pad_widgetbook.dart b/example/widgetbook/pages/components/dial_pad_widgetbook.dart new file mode 100644 index 00000000..c0460580 --- /dev/null +++ b/example/widgetbook/pages/components/dial_pad_widgetbook.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:widgetbook/widgetbook.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +import '../../test/test_components.dart'; + +WidgetbookComponent dialPadWidgetbook() { + return WidgetbookComponent( + isInitiallyExpanded: false, + name: 'Dial Pad', + useCases: [ + WidgetbookUseCase( + name: 'Dial Pad', + builder: (context) { + return WidgetbookTestWidget( + widget: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ZetaDialPad( + buttonValues: context.knobs.boolean(label: 'Change to emoji') + ? { + 'πŸ˜€': '', + 'πŸ₯²': '', + 'πŸ₯³': '', + '🀠': '', + '😨': '', + 'πŸ‘€': '', + '🐀': '', + '🐞': '', + '🦊': '', + 'πŸ†': '', + '⛺️': '', + '🧽': '' + } + : null, + buttonsPerRow: context.knobs.int.slider(label: 'Buttons per row', initialValue: 3, min: 1, max: 9), + ), + ], + ).paddingAll(ZetaSpacing.l), + ); + }, + ), + ], + ); +} diff --git a/example/widgetbook/widgetbook.dart b/example/widgetbook/widgetbook.dart index ff8168f4..2179ec35 100644 --- a/example/widgetbook/widgetbook.dart +++ b/example/widgetbook/widgetbook.dart @@ -9,6 +9,7 @@ import 'pages/components/badges_widgetbook.dart'; import 'pages/components/bottom_sheet_widgetbook.dart'; import 'pages/components/button_widgetbook.dart'; import 'pages/components/checkbox_widgetbook.dart'; +import 'pages/components/dial_pad_widgetbook.dart'; import 'pages/theme/color_widgetbook.dart'; import 'pages/components/banner_widgetbook.dart'; import 'pages/components/chip_widgetbook.dart'; @@ -39,6 +40,7 @@ class HotReload extends StatelessWidget { chipWidgetBook(), passwordInputWidgetBook(), bottomSheetWidgetBook(), + dialPadWidgetbook(), ]..sort((a, b) => a.name.compareTo(b.name)), ), WidgetbookCategory( diff --git a/lib/src/components/dial_pad/dial_pad.dart b/lib/src/components/dial_pad/dial_pad.dart new file mode 100644 index 00000000..61f8a609 --- /dev/null +++ b/lib/src/components/dial_pad/dial_pad.dart @@ -0,0 +1,220 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +import '../../../zeta_flutter.dart'; +import '../../utils/utils.dart'; + +const Map _defaultButtonValues = { + '1': '', + '2': 'ABC', + '3': 'DEF', + '4': 'GHI', + '5': 'JKL', + '6': 'MNO', + '7': 'PQRS', + '8': 'TUV', + '9': 'WXYZ', + '*': '', + '0': '+', + '#': '', +}; + +const int _defaultButtonsPerRow = 3; + +/// Dial pad gives the user the ability to dial a number and start a call. It also has a quick dial security action and a delete entry action. +class ZetaDialPad extends StatefulWidget { + /// Constructs a [ZetaDialPad]. + const ZetaDialPad({ + super.key, + this.buttonsPerRow = _defaultButtonsPerRow, + this.buttonValues = _defaultButtonValues, + this.onNumber, + this.onText, + }); + + /// Callback when number is tapped. Returns the large value from the button, i,e, 1,2,3 etc. + final ValueChanged? onNumber; + + /// Callback when number is tapped. Returns the small value from the button after a small delay, i,e, a,b,c etc. + final ValueChanged? onText; + + /// Number of buttons to show on each row. Defaults to 3. + final int? buttonsPerRow; + + /// Map of values to show on the buttons. + /// + /// Key is the large character, i.e. 1, 2, 3. + /// + /// Value is the smaller character(s): i.e. 'ABC' + final Map? buttonValues; + + @override + State createState() => _ZetaDialPadState(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(ObjectFlagProperty?>.has('onInput', onNumber)) + ..add(IntProperty('buttonsPerRow', buttonsPerRow)) + ..add(DiagnosticsProperty>('buttonValues', buttonValues)) + ..add(ObjectFlagProperty?>.has('onText', onText)); + } +} + +class _ZetaDialPadState extends State { + int get _buttonsPerRow => widget.buttonsPerRow ?? _defaultButtonsPerRow; + + Map get _buttonValues => widget.buttonValues ?? _defaultButtonValues; + + String? _lastTapped; + int _tapCounter = 0; + + ZetaDebounce? _debounce; + + void onTap(String tapped) { + widget.onNumber?.call(tapped); + + if (tapped != _lastTapped) { + if (_lastTapped == null) { + _debounce = ZetaDebounce(() => _fireChar(tapped, _tapCounter)); + } else { + _debounce?.debounce(newCallback: () => _fireChar(tapped, 1)); + _fireChar(_lastTapped!, _tapCounter); + } + _tapCounter = 1; + _lastTapped = tapped; + } else if (_lastTapped != null && _lastTapped == tapped) { + _tapCounter += 1; + _debounce?.debounce(newCallback: () => _fireChar(tapped, _tapCounter)); + } + } + + void _fireChar(String lastTapped, int tapCounter) { + final letters = _buttonValues[lastTapped]; + if (letters != null && letters.isNotEmpty) { + _tapCounter = 0; + _lastTapped = null; + + final List options = letters.split(''); + final int index = (tapCounter - 1) % options.length; + + widget.onText?.call(options[index]); + } + } + + @override + Widget build(BuildContext context) { + return SelectionContainer.disabled( + child: SizedBox( + width: (_buttonsPerRow * ZetaSpacing.x16) + ((_buttonsPerRow - 1) * ZetaSpacing.x9), + child: GridView.count( + crossAxisCount: _buttonsPerRow, + shrinkWrap: true, + semanticChildCount: _buttonValues.length, + mainAxisSpacing: ZetaSpacing.x6, + crossAxisSpacing: ZetaSpacing.x9, + children: _buttonValues.entries + .map( + (e) => ZetaDialPadButton( + primary: e.key, + secondary: e.value, + onTap: onTap, + topPadding: e.key == '*' + ? 11 + : e.value.isEmpty && e.key != '1' + ? 14 + : 3, + ), + ) + .toList(), + ), + ), + ); + } +} + +/// Individual button for [ZetaDialPad]. +class ZetaDialPadButton extends StatelessWidget { + /// Constructs a [ZetaDialPadButton] + const ZetaDialPadButton({ + super.key, + required this.primary, + this.secondary = '', + required this.onTap, + this.topPadding = 3, + }); + + /// Primary value displayed on button. + /// + /// Typically a single-digit number, but can be any string. + /// + /// You should avoid using more than a single character, as this will not render correctly. + final String primary; + + /// Secondary value displayed on button, below [primary]. + /// + /// Typically letters, but can be any string. + final String secondary; + + /// Padding for top of button. Defaults to 3. + final double topPadding; + + /// Returns the number tapped when tapped. + final ValueChanged? onTap; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(ObjectFlagProperty>.has('onTap', onTap)) + ..add(StringProperty('letters', secondary)) + ..add(StringProperty('number', primary)) + ..add(DoubleProperty('topPadding', topPadding)); + } + + @override + Widget build(BuildContext context) { + final colors = Zeta.of(context).colors; + + return Semantics( + button: true, + value: primary, + excludeSemantics: true, + child: AnimatedContainer( + duration: Durations.short2, + width: ZetaSpacing.x16, + height: ZetaSpacing.x16, + decoration: ShapeDecoration( + shape: const CircleBorder(), + color: colors.warm.shade10, + shadows: [BoxShadow(color: colors.black.withOpacity(0.15), blurRadius: 4, offset: const Offset(0, 2))], + ), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () => onTap?.call(primary), + borderRadius: ZetaRadius.full, + overlayColor: MaterialStateProperty.resolveWith((states) { + if (states.contains(MaterialState.pressed)) { + return colors.surfaceSelectedHovered; + } + if (states.contains(MaterialState.hovered)) { + return colors.surfaceHovered; + } + return null; + }), + child: Column( + children: [ + SizedBox(height: topPadding), + Text(primary, style: ZetaTextStyles.heading1), + if (topPadding < 10) Text(secondary, style: ZetaTextStyles.labelIndicator), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/src/components/progress/progress.dart b/lib/src/components/progress/progress.dart index 8730a303..3ad63440 100644 --- a/lib/src/components/progress/progress.dart +++ b/lib/src/components/progress/progress.dart @@ -19,8 +19,7 @@ abstract class ZetaProgress extends StatefulWidget { /// Super class for [ZetaProgressState] /// Defines functions that deal with state change of progress value and /// animation changing. -abstract class ZetaProgressState extends State - with TickerProviderStateMixin { +abstract class ZetaProgressState extends State with TickerProviderStateMixin { /// Decimal progress value late double progress; @@ -35,7 +34,9 @@ abstract class ZetaProgressState extends State super.initState(); progress = widget.progress; controller = AnimationController( - vsync: this, duration: const Duration(milliseconds: 200)); + vsync: this, + duration: const Duration(milliseconds: 200), + ); animation = Tween( begin: widget.progress, // Start value end: widget.progress, // End value (initially same as start value) diff --git a/lib/src/components/progress/progress_bar.dart b/lib/src/components/progress/progress_bar.dart index f7793fc8..2e8ac9da 100644 --- a/lib/src/components/progress/progress_bar.dart +++ b/lib/src/components/progress/progress_bar.dart @@ -20,40 +20,41 @@ enum ZetaBarType { class ZetaProgressBar extends ZetaProgress { ///Constructor for [ZetaProgressBar] - const ZetaProgressBar( - {super.key, - required super.progress, - required this.rounded, - required this.type, - required this.isThin, - this.label}); + const ZetaProgressBar({ + super.key, + required super.progress, + required this.rounded, + required this.type, + required this.isThin, + this.label, + }); /// Constructs a standard progress bar - const ZetaProgressBar.standard( - {super.key, - required super.progress, - this.rounded = true, - this.isThin = false, - this.label}) - : type = ZetaBarType.standard; + const ZetaProgressBar.standard({ + super.key, + required super.progress, + this.rounded = true, + this.isThin = false, + this.label, + }) : type = ZetaBarType.standard; /// Constructs buffering example - const ZetaProgressBar.buffering( - {super.key, - required super.progress, - this.rounded = true, - this.isThin = false, - this.label}) - : type = ZetaBarType.buffering; + const ZetaProgressBar.buffering({ + super.key, + required super.progress, + this.rounded = true, + this.isThin = false, + this.label, + }) : type = ZetaBarType.buffering; /// Constructs indeterminate example - const ZetaProgressBar.indeterminate( - {super.key, - required super.progress, - this.rounded = true, - this.isThin = false, - this.label}) - : type = ZetaBarType.indeterminate; + const ZetaProgressBar.indeterminate({ + super.key, + required super.progress, + this.rounded = true, + this.isThin = false, + this.label, + }) : type = ZetaBarType.indeterminate; /// Is progress bar rounded or sharp. final bool rounded; @@ -91,53 +92,54 @@ class _ZetaProgressBarState extends ZetaProgressState { widget.label!, textAlign: TextAlign.start, ), - Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Expanded( - child: AnimatedContainer( - duration: const Duration(milliseconds: 500), - height: _weight, - child: LinearProgressIndicator( - borderRadius: _border, - value: widget.type == ZetaBarType.indeterminate - ? null - : animation.value, - backgroundColor: widget.type == ZetaBarType.buffering - ? Zeta.of(context).colors.surfaceDisabled - : Colors.transparent, + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: AnimatedContainer( + duration: const Duration(milliseconds: 500), + height: _weight, + child: LinearProgressIndicator( + borderRadius: _border, + value: widget.type == ZetaBarType.indeterminate ? null : animation.value, + backgroundColor: widget.type == ZetaBarType.buffering + ? Zeta.of(context).colors.surfaceDisabled + : Colors.transparent, + ), ), ), - ), - _extraWidgets(), - ]) + _extraWidgets(), + ], + ), ], ); } /// Returns border based on widgets border type. - BorderRadius get _border => - widget.rounded ? ZetaRadius.rounded : ZetaRadius.none; + BorderRadius get _border => widget.rounded ? ZetaRadius.rounded : ZetaRadius.none; /// Returns thickness of progress bar based on its weight. double get _weight => widget.isThin ? 8 : 16; Widget _extraWidgets() { - final Iterable> extraList = List.filled(3, false).map((e) => [ - const SizedBox( - width: 16, - ), - Container( - width: _weight, - height: _weight, - decoration: const BoxDecoration( - color: Color.fromRGBO(224, 227, 233, 1), - borderRadius: ZetaRadius.rounded), + final Iterable> extraList = List.filled(3, false).map( + (e) => [ + const SizedBox( + width: 16, + ), + Container( + width: _weight, + height: _weight, + decoration: const BoxDecoration( + color: Color.fromRGBO(224, 227, 233, 1), + borderRadius: ZetaRadius.rounded, ), - ]); + ), + ], + ); final Widget extraWidgets = Row( - children: widget.type == ZetaBarType.buffering - ? extraList.expand((list) => list).toList() - : [], + children: widget.type == ZetaBarType.buffering ? extraList.expand((list) => list).toList() : [], ); return extraWidgets; } diff --git a/lib/src/theme/typography.dart b/lib/src/theme/typography.dart index 62857bbe..c5515f42 100644 --- a/lib/src/theme/typography.dart +++ b/lib/src/theme/typography.dart @@ -32,7 +32,7 @@ class ZetaTextStyles { /// Headline styles are smaller than display styles. They're best-suited for /// short, high-emphasis text on smaller screens. /// {@endtemplate} - static const TextStyle heading1 = TextStyle(fontSize: 32, fontWeight: FontWeight.w500, height: 40 / 32); + static const TextStyle heading1 = TextStyle(fontSize: 32, fontWeight: FontWeight.w500, height: 36 / 32); /// Middle size of the headline styles. /// diff --git a/lib/src/utils/utils.dart b/lib/src/utils/utils.dart new file mode 100644 index 00000000..7171d6a3 --- /dev/null +++ b/lib/src/utils/utils.dart @@ -0,0 +1,37 @@ +import 'dart:async'; + +const Duration _debounceDuration = Duration(milliseconds: 500); + +/// Debounce utility +class ZetaDebounce { + /// Constructs and starts the debouncer. + factory ZetaDebounce(void Function() callback, {Duration duration = _debounceDuration}) { + return ZetaDebounce._(callback, duration)..debounce(); + } + + /// Constructs debouncer but does not initialize the timer. + ZetaDebounce.stopped(this.callback, {this.duration = _debounceDuration}); + + ZetaDebounce._(this.callback, this.duration); + + /// Function called after [Duration] has elapsed. + final void Function() callback; + + /// Duration to wait for function to be ready to send. + /// + /// Defaults to 500 milliseconds. + final Duration duration; + + Timer? _timer; + + /// Starts the debouncer. + /// + /// This function is called automatically when default factory constructor is used. + void debounce({void Function()? newCallback}) { + _timer?.cancel(); + _timer = Timer(duration, newCallback ?? callback); + } + + /// Cancels the debouncer. + void cancel() => _timer?.cancel(); +} diff --git a/lib/zeta_flutter.dart b/lib/zeta_flutter.dart index 3bffd1dd..8ee35e06 100644 --- a/lib/zeta_flutter.dart +++ b/lib/zeta_flutter.dart @@ -20,6 +20,7 @@ export 'src/components/buttons/fab.dart'; export 'src/components/buttons/icon_button.dart'; export 'src/components/checkbox/checkbox.dart'; export 'src/components/chips/chip.dart'; +export 'src/components/dial_pad/dial_pad.dart'; export 'src/components/password/password_input.dart'; export 'src/components/progress/progress_bar.dart'; export 'src/theme/color_extensions.dart';