From 6c6adafc7482dd8fcde8a954480d0c56ea61bd5b Mon Sep 17 00:00:00 2001 From: ahmed-osman3 <99483750+ahmed-osman3@users.noreply.github.com> Date: Wed, 6 Mar 2024 15:00:11 +0000 Subject: [PATCH] feat : Progress Circle (#31) Authored-by: Osman --- example/lib/home.dart | 1 - .../pages/components/progress_example.dart | 76 +++-- example/widgetbook/main.dart | 1 + .../pages/components/progress_widgetbook.dart | 25 +- lib/src/components/buttons/button_group.dart | 266 ++++++++++++++++++ lib/src/components/progress/progress.dart | 4 +- lib/src/components/progress/progress_bar.dart | 23 +- .../components/progress/progress_circle.dart | 139 +++++++++ lib/zeta_flutter.dart | 1 + 9 files changed, 500 insertions(+), 36 deletions(-) create mode 100644 lib/src/components/buttons/button_group.dart create mode 100644 lib/src/components/progress/progress_circle.dart diff --git a/example/lib/home.dart b/example/lib/home.dart index 3fb83363..34446d63 100644 --- a/example/lib/home.dart +++ b/example/lib/home.dart @@ -13,7 +13,6 @@ import 'package:zeta_example/pages/components/navigation_bar_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'; - import 'package:zeta_example/pages/assets/icons_example.dart'; import 'package:zeta_example/widgets.dart'; import 'package:zeta_flutter/zeta_flutter.dart'; diff --git a/example/lib/pages/components/progress_example.dart b/example/lib/pages/components/progress_example.dart index 84f7887f..9c0f5ef2 100644 --- a/example/lib/pages/components/progress_example.dart +++ b/example/lib/pages/components/progress_example.dart @@ -28,7 +28,11 @@ class ProgressExampleState extends State { SizedBox( height: 20, ), - Wrapper(stepsCompleted: 0, type: ZetaProgressBarType.standard, isThin: false, stateChangeable: true), + Wrapper( + stepsCompleted: 0, + type: ZetaProgressBarType.standard, + isThin: false, + stateChangeable: true), SizedBox( height: 20, ), @@ -38,6 +42,12 @@ class ProgressExampleState extends State { isThin: false, label: "UPLOADING ...", ), + Wrapper( + stepsCompleted: 0, + circleSize: ZetaCircleSizes.xl, + rounded: false, + isCircle: true, + ), ]), ), ), @@ -47,21 +57,26 @@ class ProgressExampleState extends State { } class Wrapper extends StatefulWidget { - const Wrapper( - {super.key, - required this.stepsCompleted, - this.type = ZetaProgressBarType.standard, - this.isThin = false, - this.rounded = true, - this.stateChangeable = false, - this.label}); + const Wrapper({ + super.key, + required this.stepsCompleted, + this.type = ZetaProgressBarType.standard, + this.isThin = false, + this.rounded = true, + this.stateChangeable = false, + this.label, + this.isCircle = false, + this.circleSize, + }); final int stepsCompleted; - final bool rounded; - final ZetaProgressBarType type; - final bool isThin; + final bool? rounded; + final ZetaProgressBarType? type; + final bool? isThin; final String? label; - final bool stateChangeable; + final bool? stateChangeable; + final bool isCircle; + final ZetaCircleSizes? circleSize; @override State createState() => _WrapperState(); @@ -75,7 +90,7 @@ class _WrapperState extends State { @override void initState() { super.initState(); - type = widget.type; + type = widget.type!; stepsCompleted = widget.stepsCompleted; progress = stepsCompleted / 10; } @@ -90,7 +105,9 @@ class _WrapperState extends State { void setLoading() { setState(() { - type = type == ZetaProgressBarType.buffering ? ZetaProgressBarType.standard : ZetaProgressBarType.buffering; + type = type == ZetaProgressBarType.buffering + ? ZetaProgressBarType.standard + : ZetaProgressBarType.buffering; }); } @@ -99,20 +116,35 @@ class _WrapperState extends State { return Column( // Replace with a Column for vertical children: [ - SizedBox( - width: 400, - child: ZetaProgressBar( - progress: progress, rounded: widget.rounded, type: type, isThin: widget.isThin, label: widget.label), - ), + widget.isCircle + ? Center( + child: ZetaProgressCircle( + progress: progress, + size: widget.circleSize!, + ), + ) + : SizedBox( + width: 400, + child: ZetaProgressBar( + progress: progress, + rounded: widget.rounded!, + type: type, + isThin: widget.isThin!, + label: widget.label), + ), const SizedBox(width: 40), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ widget.type != ZetaProgressBarType.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")) + : SizedBox.shrink() ], ) ], diff --git a/example/widgetbook/main.dart b/example/widgetbook/main.dart index fcce448d..a976b089 100644 --- a/example/widgetbook/main.dart +++ b/example/widgetbook/main.dart @@ -84,6 +84,7 @@ class HotReload extends StatelessWidget { name: 'Progress', useCases: [ WidgetbookUseCase(name: 'Bar', builder: (context) => progressBarUseCase(context)), + WidgetbookUseCase(name : 'Circle', builder : (context) => progressCircleUseCase(context)) ], ), ]..sort((a, b) => a.name.compareTo(b.name)), diff --git a/example/widgetbook/pages/components/progress_widgetbook.dart b/example/widgetbook/pages/components/progress_widgetbook.dart index 06a007c5..3dfcbb83 100644 --- a/example/widgetbook/pages/components/progress_widgetbook.dart +++ b/example/widgetbook/pages/components/progress_widgetbook.dart @@ -9,9 +9,13 @@ Widget progressBarUseCase(BuildContext context) => WidgetbookTestWidget( return SizedBox( width: constraints.maxWidth - ZetaSpacing.xl, child: ZetaProgressBar( - progress: context.knobs.double.slider(label: 'Progress', min: 0, max: 1, initialValue: 0.5).toDouble(), - type: context.knobs - .list(label: 'Type', options: ZetaProgressBarType.values, labelBuilder: (value) => value.name), + progress: context.knobs.double + .slider(label: 'Progress', min: 0, max: 1, initialValue: 0.5) + .toDouble(), + type: context.knobs.list( + label: 'Type', + options: ZetaProgressBarType.values, + labelBuilder: (value) => value.name), isThin: context.knobs.boolean(label: 'Thin'), rounded: context.knobs.boolean(label: 'Rounded'), label: context.knobs.stringOrNull(label: 'Label'), @@ -19,3 +23,18 @@ Widget progressBarUseCase(BuildContext context) => WidgetbookTestWidget( ); }), ); + +Widget progressCircleUseCase(BuildContext context) => + WidgetbookTestWidget(widget: + ZetaProgressCircle( + progress: context.knobs.double + .slider(label: 'Progress', min: 0, max: 1, initialValue: 0.5) + .toDouble(), + rounded: context.knobs.boolean(label: 'Rounded'), + size: context.knobs.list( + initialOption: ZetaCircleSizes.xl, + label: 'Size', + options: ZetaCircleSizes.values, + labelBuilder: (value) => value.name), + ), + ); diff --git a/lib/src/components/buttons/button_group.dart b/lib/src/components/buttons/button_group.dart new file mode 100644 index 00000000..232ce60d --- /dev/null +++ b/lib/src/components/buttons/button_group.dart @@ -0,0 +1,266 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import '../../../zeta_flutter.dart'; + +/// Zeta Button Group +class ZetaButtonGroup extends StatelessWidget { + /// Constructs [ZetaButtonGroup] from a list of [GroupButton]s + const ZetaButtonGroup( + {super.key, + required this.buttons, + required this.rounded, + required this.isLarge,}); + + /// Determines size of [GroupButton] + final bool isLarge; + + /// Determinses border radius of [GroupButton] + final bool rounded; + + /// [GroupButton]s to be rendered in list + final List buttons; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: getButtons(), + ); + } + + /// Returns [GroupButton]s with there appropriate styling. + List getButtons() { + for (final element in buttons) { + element + .._isInitial = element._isFinal = false + .._isLarge = isLarge + .._rounded = rounded; + } + + buttons.first._isInitial = true; + buttons.last._isFinal = true; + + return buttons; + } + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('isLarge', isLarge)) + ..add(DiagnosticsProperty('rounded', rounded)) + ; + } +} + +/// Group Button item +// ignore: must_be_immutable +class GroupButton extends StatefulWidget { + /// Constructs [GroupButton] + GroupButton({ + super.key, + this.label, + this.icon, + this.onPress, + }); + + /// Constructs dropdown group button + GroupButton.dropdown({ + super.key, + required this.onPress, + this.icon, + this.label, + }); + + ///Constructs group button with icon + GroupButton.icon({ + super.key, + required this.icon, + this.onPress, + this.label, + }); + + /// Label for [GroupButton] + final String? label; + + /// Optional icon for [GroupButton] + final IconData? icon; + + /// Function for when [GroupButton] is clicked. + final VoidCallback? onPress; + + ///If [GroupButton] is large + bool _isLarge = false; + + ///If [GroupButton] is rounded + bool _rounded = false; + + /// If [GroupButton] is the first button in its list. + bool _isInitial = false; + + /// If [GroupButton] is the final button in its list. + bool _isFinal = false; + + @override + State createState() => _GroupButtonState(); + + /// Returns copy of [GroupButton] with fields. + GroupButton copyWith({bool? isFinal, bool? isInitial}) { + return GroupButton( + key: key, + label: label, + icon: icon, + onPress: onPress, + // isFinal: isFinal ?? this.isFinal, + // isInitial: isInitial ?? this.isInitial, + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(StringProperty('Label', label)) + ..add(DiagnosticsProperty('icon', icon)) + ..add(ObjectFlagProperty.has('onPress', onPress)); + } +} + +class _GroupButtonState extends State { + late bool selected; + late MaterialStatesController controller; + + @override + void initState() { + super.initState(); + selected = false; + controller = MaterialStatesController(); + } + + void onPress() { + widget.onPress!(); + setState(() { + selected = !selected; + }); + } + + @override + Widget build(BuildContext context) { + final colors = Zeta.of(context).colors; + + final borderType = + widget._rounded ? ZetaWidgetBorder.rounded : ZetaWidgetBorder.sharp; + + final BorderSide borderSide = + _getBorderSide(controller.value, colors, false); + + return Container( + decoration: BoxDecoration( + border: Border( + top: borderSide, + left: borderSide, + bottom: borderSide, + right: (widget._isFinal) ? borderSide : BorderSide.none, + ), + borderRadius: _getRadius(borderType), + ), + padding: EdgeInsets.zero, + child: FilledButton( + statesController: controller, + onPressed: onPress, + style: getStyle(borderType, colors), + child: SelectionContainer.disabled( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (widget.icon != null) Icon(widget.icon), + Text(widget.label!), + if (widget.onPress != null) + const Icon(ZetaIcons.expand_more_round), + ], + ).paddingAll(_padding), + ), + ), + ); + } + + double get _padding => widget._isLarge ? ZetaSpacing.x4 : ZetaSpacing.x3; + + BorderSide _getBorderSide( + Set states, + ZetaColors colors, + bool finalButton, + ) { + if (states.contains(MaterialState.disabled)) { + return BorderSide(color: colors.cool.shade40); + } + if (states.contains(MaterialState.focused)) { + return BorderSide(color: colors.blue, width: ZetaSpacing.x0_5); + } + return BorderSide( + color: finalButton ? colors.borderDefault : colors.borderSubtle, + ); + } + + BorderRadius _getRadius(ZetaWidgetBorder borderType) { + if (widget._isInitial) { + return borderType.radius.copyWith( + topRight: Radius.zero, + bottomRight: Radius.zero, + ); + } + if (widget._isFinal) { + return borderType.radius.copyWith( + topLeft: Radius.zero, + bottomLeft: Radius.zero, + ); + } + return ZetaRadius.none; + } + + ButtonStyle getStyle(ZetaWidgetBorder borderType, ZetaColors colors) { + final ZetaColorSwatch color = + selected ? colors.cool : ZetaColorSwatch.fromColor(colors.black); + + return ButtonStyle( + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: borderType.radius, + ), + ), + backgroundColor: MaterialStateProperty.resolveWith((states) { + if (selected) return colors.black; + + if (states.contains(MaterialState.disabled)) { + return colors.surfaceDisabled; + } + if (states.contains(MaterialState.pressed)) { + return colors.primary.shade10; + } + if (states.contains(MaterialState.hovered)) { + return colors.cool.shade20; + } + return colors.white; + }), + foregroundColor: MaterialStateProperty.resolveWith((states) { + if (states.contains(MaterialState.disabled)) { + return colors.textDisabled; + } + if (selected) return color.onColor; + return colors.textDefault; + }), + elevation: const MaterialStatePropertyAll(0), + padding: MaterialStateProperty.all(EdgeInsets.zero), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('selected', selected)) + ..add( + DiagnosticsProperty('controller', controller), + ); + } +} diff --git a/lib/src/components/progress/progress.dart b/lib/src/components/progress/progress.dart index 3ad63440..732da64c 100644 --- a/lib/src/components/progress/progress.dart +++ b/lib/src/components/progress/progress.dart @@ -34,9 +34,7 @@ abstract class ZetaProgressState extends State with T 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 d6e91502..27774711 100644 --- a/lib/src/components/progress/progress_bar.dart +++ b/lib/src/components/progress/progress_bar.dart @@ -98,7 +98,8 @@ class _ZetaProgressBarState extends ZetaProgressState { children: [ Text( widget.label ?? - (widget.label == null && widget.type != ZetaProgressBarType.indeterminate + (widget.label == null && + widget.type != ZetaProgressBarType.indeterminate ? '${(animation.value * 100).toInt()}%' : ''), style: ZetaTextStyles.titleMedium, @@ -114,13 +115,17 @@ class _ZetaProgressBarState extends ZetaProgressState { height: _weight, child: LinearProgressIndicator( borderRadius: _border, - value: widget.type == ZetaProgressBarType.indeterminate ? null : animation.value, - backgroundColor: - widget.type == ZetaProgressBarType.buffering ? colors.surfaceDisabled : Colors.transparent, + value: widget.type == ZetaProgressBarType.indeterminate + ? null + : animation.value, + backgroundColor: widget.type == ZetaProgressBarType.buffering + ? colors.surfaceDisabled + : Colors.transparent, ), ), ), - if (widget.type == ZetaProgressBarType.buffering) bufferingWidget(colors), + if (widget.type == ZetaProgressBarType.buffering) + bufferingWidget(colors), ], ), ], @@ -128,7 +133,8 @@ class _ZetaProgressBarState extends ZetaProgressState { } /// 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 ? ZetaSpacing.x2 : ZetaSpacing.x4; @@ -141,7 +147,10 @@ class _ZetaProgressBarState extends ZetaProgressState { Container( width: _weight, height: _weight, - decoration: BoxDecoration(color: colors.surfaceDisabled, borderRadius: ZetaRadius.rounded), + decoration: BoxDecoration( + color: colors.surfaceDisabled, + borderRadius: ZetaRadius.rounded, + ), ), ], ); diff --git a/lib/src/components/progress/progress_circle.dart b/lib/src/components/progress/progress_circle.dart new file mode 100644 index 00000000..0156ffcc --- /dev/null +++ b/lib/src/components/progress/progress_circle.dart @@ -0,0 +1,139 @@ +import 'dart:math' as math; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import '../../../zeta_flutter.dart'; +import 'progress.dart'; + +/// Sizes for [ZetaProgressCircle] +enum ZetaCircleSizes { + ///24 X 24 + xs, + + /// 36 X 36 + s, + + /// 40 x 40 + m, + + /// 48 X 48 + l, + + /// 64 X 64 + xl +} + +///Class definition for [ZetaProgressCircle] +class ZetaProgressCircle extends ZetaProgress { + /// Constructor for [ZetaProgressCircle] + const ZetaProgressCircle({ + super.key, + super.progress = 0, + this.size = ZetaCircleSizes.xl, + this.rounded = true, + }); + + ///Size of [ZetaProgressCircle] + final ZetaCircleSizes size; + + ///{@macro zeta-component-rounded} + final bool rounded; + + @override + State createState() => ZetaProgressCircleState(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(EnumProperty('size', size)) + ..add(DoubleProperty('progress', progress)) + ..add(DiagnosticsProperty('rounded', rounded)); + } +} + +///Class definition for [ZetaProgressCircleState] +class ZetaProgressCircleState extends ZetaProgressState { + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: BoxConstraints.tight(_getSize()), + child: AnimatedBuilder( + animation: controller, + builder: (context, child) { + return CustomPaint( + size: _getSize(), + painter: CirclePainter( + progress: animation.value, + rounded: widget.rounded, + colors: Zeta.of(context).colors, + ), + ); + }, + ), + ); + } + + Size _getSize() { + switch (widget.size) { + case ZetaCircleSizes.xs: + return const Size(ZetaSpacing.x6, ZetaSpacing.x6); + case ZetaCircleSizes.s: + return const Size(ZetaSpacing.x9, ZetaSpacing.x9); + case ZetaCircleSizes.m: + return const Size(ZetaSpacing.x10, ZetaSpacing.x10); + case ZetaCircleSizes.l: + return const Size(ZetaSpacing.x12, ZetaSpacing.x12); + case ZetaCircleSizes.xl: + return const Size(ZetaSpacing.x16, ZetaSpacing.x16); + } + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DoubleProperty('progress', progress)) + ..add(DiagnosticsProperty('controller', controller)) + ..add(DiagnosticsProperty>('animation', animation)); + } +} + +///Class definition for [CirclePainter] +class CirclePainter extends CustomPainter { + ///Constructor for [CirclePainter] + CirclePainter({this.progress = 0, this.rounded = true,required this.colors}); + + ///Percentage of progress in decimal value, defaults to 0 + final double progress; + + ///Is circle rounded, defaults to true + final bool rounded; + + /// ZetaColors + final ZetaColors colors; + + final _paint = Paint() + ..strokeWidth = 4 + ..style = PaintingStyle.stroke; + + @override + void paint(Canvas canvas, Size size) { + if (rounded) _paint.strokeCap = StrokeCap.round; + _paint.color = colors.primary; + + + const double fullCircle = 2 * math.pi; + + canvas.drawArc( + Rect.fromLTRB(0, 0, size.width, size.height), + 3 * math.pi / 2, + progress * fullCircle, + false, + _paint, + ); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) => false; +} diff --git a/lib/zeta_flutter.dart b/lib/zeta_flutter.dart index e4f60cf4..b7385da5 100644 --- a/lib/zeta_flutter.dart +++ b/lib/zeta_flutter.dart @@ -24,6 +24,7 @@ export 'src/components/dial_pad/dial_pad.dart'; export 'src/components/navigation bar/navigation_bar.dart'; export 'src/components/password/password_input.dart'; export 'src/components/progress/progress_bar.dart'; +export 'src/components/progress/progress_circle.dart'; export 'src/theme/color_extensions.dart'; export 'src/theme/color_scheme.dart'; export 'src/theme/color_swatch.dart';