From 6394ef6b2a96bd919149e0d9e139815615ddf715 Mon Sep 17 00:00:00 2001 From: ahmed-osman3 <99483750+ahmed-osman3@users.noreply.github.com> Date: Fri, 14 Jun 2024 10:58:49 +0100 Subject: [PATCH] feat: create slider component (#53) --- example/lib/home.dart | 3 + .../lib/pages/components/slider_example.dart | 33 ++++ example/widgetbook/main.dart | 2 + .../pages/components/slider_widgetbook.dart | 39 +++++ lib/src/components/slider/slider.dart | 148 ++++++++++++++++++ lib/zeta_flutter.dart | 1 + 6 files changed, 226 insertions(+) create mode 100644 example/lib/pages/components/slider_example.dart create mode 100644 example/widgetbook/pages/components/slider_widgetbook.dart create mode 100644 lib/src/components/slider/slider.dart diff --git a/example/lib/home.dart b/example/lib/home.dart index 4f43ba7b..4414e8b4 100644 --- a/example/lib/home.dart +++ b/example/lib/home.dart @@ -26,8 +26,10 @@ import 'package:zeta_example/pages/components/phone_input_example.dart'; import 'package:zeta_example/pages/components/radio_example.dart'; import 'package:zeta_example/pages/components/screen_header_bar_example.dart'; import 'package:zeta_example/pages/components/select_input_example.dart'; + import 'package:zeta_example/pages/components/search_bar_example.dart'; import 'package:zeta_example/pages/components/segmented_control_example.dart'; +import 'package:zeta_example/pages/components/slider_example.dart'; import 'package:zeta_example/pages/components/stepper_example.dart'; import 'package:zeta_example/pages/components/stepper_input_example.dart'; import 'package:zeta_example/pages/components/switch_example.dart'; @@ -84,6 +86,7 @@ final List components = [ Component(DialPadExample.name, (context) => const DialPadExample()), Component(RadioButtonExample.name, (context) => const RadioButtonExample()), Component(SwitchExample.name, (context) => const SwitchExample()), + Component(SliderExample.name, (context) => const SliderExample()), Component(DateInputExample.name, (context) => const DateInputExample()), Component(PhoneInputExample.name, (context) => const PhoneInputExample()), Component(DialogExample.name, (context) => const DialogExample()), diff --git a/example/lib/pages/components/slider_example.dart b/example/lib/pages/components/slider_example.dart new file mode 100644 index 00000000..e9850bd8 --- /dev/null +++ b/example/lib/pages/components/slider_example.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +import 'package:zeta_example/widgets.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +class SliderExample extends StatefulWidget { + static const String name = 'Slider'; + + const SliderExample({super.key}); + + @override + State createState() => _SliderExampleState(); +} + +class _SliderExampleState extends State { + double value = 0.5; + + @override + Widget build(BuildContext context) { + return ExampleScaffold( + name: SliderExample.name, + child: Center( + child: ZetaSlider( + value: value, + onChange: (newValue) { + setState(() { + value = newValue; + }); + }, + ), + ), + ); + } +} diff --git a/example/widgetbook/main.dart b/example/widgetbook/main.dart index ee79298c..787ef923 100644 --- a/example/widgetbook/main.dart +++ b/example/widgetbook/main.dart @@ -9,6 +9,7 @@ import 'pages/assets/icon_widgetbook.dart'; import 'pages/components/accordion_widgetbook.dart'; import 'pages/components/dropdown_list_item_widgetbook.dart'; import 'pages/components/notification_list_item_widgetbook.dart'; +import 'pages/components/slider_widgetbook.dart'; import 'pages/components/text_input_widgetbook.dart'; import 'pages/components/top_app_bar_widgetbook.dart'; import 'pages/components/avatar_widgetbook.dart'; @@ -201,6 +202,7 @@ class _HotReloadState extends State { WidgetbookUseCase(name: 'Search Bar', builder: (context) => searchBarUseCase(context)), WidgetbookUseCase(name: 'Segmented Control', builder: (context) => segmentedControlUseCase(context)), WidgetbookUseCase(name: 'Select Input', builder: (context) => selectInputUseCase(context)), + WidgetbookUseCase(name: 'Slider', builder: (context) => sliderUseCase(context)), WidgetbookUseCase(name: 'Snack Bar', builder: (context) => snackBarUseCase(context)), WidgetbookUseCase(name: 'Stepper Input', builder: (context) => stepperInputUseCase(context)), WidgetbookUseCase(name: 'Stepper', builder: (context) => stepperUseCase(context)), diff --git a/example/widgetbook/pages/components/slider_widgetbook.dart b/example/widgetbook/pages/components/slider_widgetbook.dart new file mode 100644 index 00000000..0b0f6ed1 --- /dev/null +++ b/example/widgetbook/pages/components/slider_widgetbook.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:widgetbook/widgetbook.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +import '../../test/test_components.dart'; + +Widget sliderUseCase(BuildContext context) { + return WidgetbookTestWidget(widget: Builder(builder: (context) { + return ZetaSliderExample(context); + })); +} + +class ZetaSliderExample extends StatefulWidget { + const ZetaSliderExample(this.c); + final BuildContext c; + + @override + State createState() => _ZetaSliderExampleState(); +} + +class _ZetaSliderExampleState extends State { + double value = 0.5; + + @override + Widget build(BuildContext context) { + return ZetaSlider( + value: value, + rounded: widget.c.knobs.boolean(label: "Rounded"), + divisions: widget.c.knobs.intOrNull.slider(label: "Divisions", min: 1, initialValue: 10), + onChange: widget.c.knobs.boolean(label: "Disabled") + ? null + : (newValue) { + setState(() { + value = newValue; + }); + }, + ); + } +} diff --git a/lib/src/components/slider/slider.dart b/lib/src/components/slider/slider.dart new file mode 100644 index 00000000..45df0b65 --- /dev/null +++ b/lib/src/components/slider/slider.dart @@ -0,0 +1,148 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import '../../theme/tokens.dart'; +import '../../zeta.dart'; + +/// Slider component with customized styling +class ZetaSlider extends StatefulWidget { + /// Default constructor for [ZetaSlider] + const ZetaSlider({ + super.key, + required this.value, + this.onChange, + this.rounded = false, + this.divisions, + }); + + /// Double value to represent slider percentage + final double value; + + /// Callback to handle changing of slider + final ValueChanged? onChange; + + /// {@macro zeta-component-rounded} + final bool rounded; + + /// Number of divisions. + final int? divisions; + + @override + State createState() => _ZetaSliderState(); + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('rounded', rounded)) + ..add(DoubleProperty('value', value)) + ..add(ObjectFlagProperty?>.has('onChange', onChange)) + ..add(IntProperty('divisions', divisions)); + } +} + +class _ZetaSliderState extends State { + bool _selected = false; + + @override + Widget build(BuildContext context) { + final colors = Zeta.of(context).colors; + + return SliderTheme( + data: SliderThemeData( + /** TODO: Match with new colors */ + + /// Active Track + activeTrackColor: _activeColor, + disabledActiveTrackColor: colors.surfaceDisabled, + + /// Inactive Track + inactiveTrackColor: colors.surfaceInfoSubtle, + + /// Ticks + activeTickMarkColor: colors.surfaceDefault, + inactiveTickMarkColor: colors.surfaceDefault, + + /// Thumb + thumbColor: colors.surfaceDefaultInverse, + disabledThumbColor: colors.surfaceDisabled, + overlayShape: SliderThumb(size: ZetaSpacing.xl_1, rounded: widget.rounded, color: _activeColor), + thumbShape: SliderThumb( + size: ZetaSpacing.large, + rounded: widget.rounded, + color: _activeColor, + ), + ), + child: Slider( + value: widget.value, + onChanged: widget.onChange, + divisions: widget.divisions, + onChangeStart: (_) { + setState(() { + _selected = true; + }); + }, + onChangeEnd: (_) { + setState(() { + _selected = false; + }); + }, + ), + ); + } + + Color get _activeColor { + final colors = Zeta.of(context).colors; + if (widget.onChange == null) { + return colors.surfaceDisabled; + } + return _selected ? colors.primary : colors.surfaceDefaultInverse; + } +} + +/// Custom slider thumb component +class SliderThumb extends SliderComponentShape { + /// Constructor for [SliderThumb] + const SliderThumb({required this.size, required this.rounded, required this.color}); + + /// Radius or width/height for [SliderThumb] depending on shape + final double size; + + /// If [SliderThumb] is circular or a square + final bool rounded; + + /// Color of [SliderThumb] + final Color color; + + @override + Size getPreferredSize(bool isEnabled, bool isDiscrete) { + return Size.fromRadius(size); + } + + /// Paints thumb + @override + void paint( + PaintingContext context, + Offset center, { + required Animation activationAnimation, + required Animation enableAnimation, + required bool isDiscrete, + required TextPainter labelPainter, + required RenderBox parentBox, + required SliderThemeData sliderTheme, + required TextDirection textDirection, + required double value, + required double textScaleFactor, + required Size sizeWithOverflow, + }) { + final Canvas canvas = context.canvas; + + final paint = Paint() + ..style = PaintingStyle.fill + ..color = color; + + // draw icon with text painter + rounded + ? canvas.drawCircle(center, size, paint) + : canvas.drawRect(Rect.fromCenter(center: center, width: size, height: size), paint); + } +} diff --git a/lib/zeta_flutter.dart b/lib/zeta_flutter.dart index 9bbb1ca6..f3d73c56 100644 --- a/lib/zeta_flutter.dart +++ b/lib/zeta_flutter.dart @@ -45,6 +45,7 @@ export 'src/components/screen_header_bar/screen_header_bar.dart'; export 'src/components/search_bar/search_bar.dart'; export 'src/components/segmented_control/segmented_control.dart'; export 'src/components/select_input/select_input.dart'; +export 'src/components/slider/slider.dart'; export 'src/components/snack_bar/snack_bar.dart'; export 'src/components/stepper/stepper.dart'; export 'src/components/stepper_input/stepper_input.dart';