From 529624730f4796eadc424301e34836063212a475 Mon Sep 17 00:00:00 2001 From: Siqlain Hanif <53166182+Siqlain-Hanif@users.noreply.github.com> Date: Sat, 29 Oct 2022 00:08:57 +0500 Subject: [PATCH] Added custom iconed checkbox that can control its active, inactive and tristae icon --- example/lib/main.dart | 3 + example/pubspec.lock | 33 +- lib/src/fields/form_builder_checkbox.dart | 26 +- .../fields/form_builder_checkbox_group.dart | 23 + lib/src/painter/radial_reaction_painter.dart | 80 +++ lib/src/widgets/custom_iconed_checkbox.dart | 600 ++++++++++++++++++ .../custom_iconed_checkbox_list_tile.dart | 253 ++++++++ lib/src/widgets/grouped_checkbox.dart | 25 +- pubspec.lock | 31 +- pubspec.yaml | 2 +- 10 files changed, 1048 insertions(+), 28 deletions(-) create mode 100644 lib/src/painter/radial_reaction_painter.dart create mode 100644 lib/src/widgets/custom_iconed_checkbox.dart create mode 100644 lib/src/widgets/custom_iconed_checkbox_list_tile.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index e127b814ca..a292c785b2 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -144,6 +144,7 @@ class _CompleteFormState extends State { name: 'accept_terms', initialValue: false, onChanged: _onChanged, + // activeIcon: Icons.text_snippet, title: RichText( text: const TextSpan( children: [ @@ -275,6 +276,8 @@ class _CompleteFormState extends State { decoration: const InputDecoration( labelText: 'The language of my people'), name: 'languages', + materialTapTargetSize: MaterialTapTargetSize.padded, + // initialValue: const ['Dart'], options: const [ FormBuilderFieldOption(value: 'Dart'), diff --git a/example/pubspec.lock b/example/pubspec.lock index 8cf19da6b2..173a66df48 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -7,7 +7,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.9.0" + version: "2.8.2" boolean_selector: dependency: transitive description: @@ -21,14 +21,21 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.2.1" + version: "1.2.0" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.1" clock: dependency: transitive description: name: clock url: "https://pub.dartlang.org" source: hosted - version: "1.1.1" + version: "1.1.0" collection: dependency: transitive description: @@ -49,7 +56,7 @@ packages: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.3.1" + version: "1.3.0" flutter: dependency: "direct main" description: flutter @@ -61,7 +68,7 @@ packages: path: ".." relative: true source: path - version: "7.7.0" + version: "7.7.1" flutter_lints: dependency: "direct dev" description: @@ -106,28 +113,28 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.12" + version: "0.12.11" material_color_utilities: dependency: transitive description: name: material_color_utilities url: "https://pub.dartlang.org" source: hosted - version: "0.1.5" + version: "0.1.4" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "1.7.0" path: dependency: transitive description: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.2" + version: "1.8.1" sky_engine: dependency: transitive description: flutter @@ -139,7 +146,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.9.0" + version: "1.8.2" stack_trace: dependency: transitive description: @@ -160,21 +167,21 @@ packages: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.1.1" + version: "1.1.0" term_glyph: dependency: transitive description: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.2.1" + version: "1.2.0" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.12" + version: "0.4.9" vector_math: dependency: transitive description: diff --git a/lib/src/fields/form_builder_checkbox.dart b/lib/src/fields/form_builder_checkbox.dart index 6592a75232..8bc6376f70 100644 --- a/lib/src/fields/form_builder_checkbox.dart +++ b/lib/src/fields/form_builder_checkbox.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:flutter_form_builder/src/widgets/custom_iconed_checkbox_list_tile.dart'; /// Single Checkbox field class FormBuilderCheckbox extends FormBuilderField { @@ -66,6 +67,21 @@ class FormBuilderCheckbox extends FormBuilderField { /// Normally, this property is left to its default value, false. final bool selected; + ///Default Flutter Icons are supported for now + ///Icon when the checkbox is in activestate + final IconData? activeIcon; + + ///Default Flutter Icons are supported for now + ///Icon when the checkbox is in inactivestate + final IconData? inactiveIcon; + + ///Default Flutter Icons are supported for now + ///Icon when the checkbox is in triactivestate + final IconData? tristateIcon; + + ///Controls the borderRadius of the iconbox + final BorderRadius? borderRadius; + /// Creates a single Checkbox field FormBuilderCheckbox({ //From Super @@ -98,6 +114,10 @@ class FormBuilderCheckbox extends FormBuilderField { this.shouldRequestFocus = false, this.subtitle, this.tristate = false, + this.activeIcon, + this.inactiveIcon, + this.tristateIcon, + this.borderRadius, }) : super( key: key, initialValue: initialValue, @@ -116,7 +136,7 @@ class FormBuilderCheckbox extends FormBuilderField { return InputDecorator( decoration: state.decoration, - child: CheckboxListTile( + child: CustomIconedCheckboxListTile( dense: true, isThreeLine: false, title: title, @@ -130,6 +150,10 @@ class FormBuilderCheckbox extends FormBuilderField { state.didChange(value); } : null, + borderRadius: borderRadius, + activeIcon: activeIcon, + inactiveIcon: inactiveIcon, + tristateIcon: tristateIcon, checkColor: checkColor, activeColor: activeColor, secondary: secondary, diff --git a/lib/src/fields/form_builder_checkbox_group.dart b/lib/src/fields/form_builder_checkbox_group.dart index 8c5d31d0ad..0a373cde76 100644 --- a/lib/src/fields/form_builder_checkbox_group.dart +++ b/lib/src/fields/form_builder_checkbox_group.dart @@ -25,6 +25,21 @@ class FormBuilderCheckboxGroup extends FormBuilderField> { final OptionsOrientation orientation; final bool shouldRequestFocus; + ///Controls the borderRadius of the iconbox + final BorderRadius? borderRadius; + + ///Default Flutter Icons are supported for now + ///Icon when the checkbox is in activestate + final IconData? activeIcon; + + ///Default Flutter Icons are supported for now + ///Icon when the checkbox is in inactivestate + final IconData? inactiveIcon; + + ///Default Flutter Icons are supported for now + ///Icon when the checkbox is in triactivestate + final IconData? tristateIcon; + /// Creates a list of Checkboxes for selecting multiple options FormBuilderCheckboxGroup({ Key? key, @@ -60,6 +75,10 @@ class FormBuilderCheckboxGroup extends FormBuilderField> { this.controlAffinity = ControlAffinity.leading, this.orientation = OptionsOrientation.wrap, this.shouldRequestFocus = false, + this.activeIcon, + this.inactiveIcon, + this.tristateIcon, + this.borderRadius, }) : super( key: key, initialValue: initialValue, @@ -107,6 +126,10 @@ class FormBuilderCheckboxGroup extends FormBuilderField> { wrapVerticalDirection: wrapVerticalDirection, separator: separator, controlAffinity: controlAffinity, + activeIcon: activeIcon, + inactiveIcon: inactiveIcon, + tristateIcon: tristateIcon, + borderRadius: borderRadius, ), ); }, diff --git a/lib/src/painter/radial_reaction_painter.dart b/lib/src/painter/radial_reaction_painter.dart new file mode 100644 index 0000000000..4b8af8c1af --- /dev/null +++ b/lib/src/painter/radial_reaction_painter.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; + +class RadialReactionPainter extends CustomPainter { + Color? _hoverColor; + Color get hoverColor => _hoverColor!; + set hoverColor(Color? value) { + if (value == _hoverColor) { + return; + } + _hoverColor = value; + } + + Color? _focusColor; + Color get focusColor => _focusColor!; + set focusColor(Color? value) { + if (value == _focusColor) { + return; + } + _focusColor = value; + } + + bool? _isFocused; + bool get isFocused => _isFocused!; + set isFocused(bool? value) { + if (value == _isFocused) { + return; + } + _isFocused = value; + } + + bool? _isHovered; + bool get isHovered => _isHovered!; + set isHovered(bool? value) { + if (value == _isHovered) { + return; + } + _isHovered = value; + } + + Offset? _downPosition; + Offset? get downPosition => _downPosition; + set downPosition(Offset? value) { + if (value == _downPosition) { + return; + } + _downPosition = value; + } + + double? _splashRadius; + double get splashRadius => _splashRadius!; + set splashRadius(double? value) { + if (value == _splashRadius) { + return; + } + _splashRadius = value; + } + + @override + void paint(Canvas canvas, Size size) { + final Offset center = + Offset.lerp(downPosition, size.center(Offset.zero), 1.0)!; + if (isFocused) { + var radialFocusReactionPaint = Paint() + ..color = focusColor + ..style = PaintingStyle.fill; + + return canvas.drawCircle(center, splashRadius, radialFocusReactionPaint); + } + if (isHovered) { + var radialHoverReactionPaint = Paint() + ..color = hoverColor + ..style = PaintingStyle.fill; + + return canvas.drawCircle(center, splashRadius, radialHoverReactionPaint); + } + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) => true; +} diff --git a/lib/src/widgets/custom_iconed_checkbox.dart b/lib/src/widgets/custom_iconed_checkbox.dart new file mode 100644 index 0000000000..a5915a0a80 --- /dev/null +++ b/lib/src/widgets/custom_iconed_checkbox.dart @@ -0,0 +1,600 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/semantics.dart'; +import 'package:flutter_form_builder/src/painter/radial_reaction_painter.dart'; + +///API for [CustomIconedCheckbox] is same as the Material checkbox will one or two differences + +/// A Material Design checkbox. +/// +/// The checkbox itself does not maintain any state. Instead, when the state of +/// the checkbox changes, the widget calls the [onChanged] callback. Most +/// widgets that use a checkbox will listen for the [onChanged] callback and +/// rebuild the checkbox with a new [value] to update the visual appearance of +/// the checkbox. +/// +/// The checkbox can optionally display three values - true, false, and null - +/// if [tristate] is true. When [value] is null a dash is displayed. By default +/// [tristate] is false and the checkbox's [value] must be true or false. +/// +/// Requires one of its ancestors to be a [Material] widget. +/// +/// {@tool dartpad} +/// This example shows how you can override the default theme of +/// of a [Checkbox] with a [MaterialStateProperty]. +/// In this example, the checkbox's color will be `Colors.blue` when the [Checkbox] +/// is being pressed, hovered, or focused. Otherwise, the checkbox's color will +/// be `Colors.red` +class CustomIconedCheckbox extends StatefulWidget { + const CustomIconedCheckbox({ + super.key, + required this.value, + this.tristate = false, + required this.onChanged, + this.mouseCursor, + this.activeColor, + this.fillColor, + this.checkColor, + this.focusColor, + this.hoverColor, + this.overlayColor, + this.splashRadius, + this.materialTapTargetSize, + this.visualDensity, + this.focusNode, + this.autofocus = false, + this.borderRadius, + this.activeIcon, + this.inactiveIcon, + this.tristateIcon, + }) : assert(tristate || value != null); + + /// Whether this checkbox is checked. + /// + /// This property must not be null. + final bool? value; + + /// Called when the value of the checkbox should change. + /// + /// The checkbox passes the new value to the callback but does not actually + /// change state until the parent widget rebuilds the checkbox with the new + /// value. + /// + /// If this callback is null, the checkbox will be displayed as disabled + /// and will not respond to input gestures. + /// + /// When the checkbox is tapped, if [tristate] is false (the default) then + /// the [onChanged] callback will be applied to `!value`. If [tristate] is + /// true this callback cycle from false to true to null. + /// + /// The callback provided to [onChanged] should update the state of the parent + /// [StatefulWidget] using the [State.setState] method, so that the parent + /// gets rebuilt; for example: + /// + /// ```dart + /// Checkbox( + /// value: _throwShotAway, + /// onChanged: (bool? newValue) { + /// setState(() { + /// _throwShotAway = newValue!; + /// }); + /// }, + /// ) + /// ``` + final ValueChanged? onChanged; + + /// {@template flutter.material.checkbox.mouseCursor} + /// The cursor for a mouse pointer when it enters or is hovering over the + /// widget. + /// + /// If [mouseCursor] is a [MaterialStateProperty], + /// [MaterialStateProperty.resolve] is used for the following [MaterialState]s: + /// + /// * [MaterialState.selected]. + /// * [MaterialState.hovered]. + /// * [MaterialState.focused]. + /// * [MaterialState.disabled]. + /// {@endtemplate} + /// + /// When [value] is null and [tristate] is true, [MaterialState.selected] is + /// included as a state. + /// + /// If null, then the value of [CheckboxThemeData.mouseCursor] is used. If + /// that is also null, then [MaterialStateMouseCursor.clickable] is used. + /// + /// See also: + /// + /// * [MaterialStateMouseCursor], a [MouseCursor] that implements + /// `MaterialStateProperty` which is used in APIs that need to accept + /// either a [MouseCursor] or a [MaterialStateProperty]. + final MouseCursor? mouseCursor; + + /// The color to use when this checkbox is checked. + /// + /// Defaults to [ThemeData.toggleableActiveColor]. + /// + /// If [fillColor] returns a non-null color in the [MaterialState.selected] + /// state, it will be used instead of this color. + final Color? activeColor; + + /// {@template flutter.material.checkbox.fillColor} + /// The color that fills the checkbox, in all [MaterialState]s. + /// + /// Resolves in the following states: + /// * [MaterialState.selected]. + /// * [MaterialState.hovered]. + /// * [MaterialState.focused]. + /// * [MaterialState.disabled]. + /// + /// {@tool snippet} + /// This example resolves the [fillColor] based on the current [MaterialState] + /// of the [Checkbox], providing a different [Color] when it is + /// [MaterialState.disabled]. + /// + /// ```dart + /// Checkbox( + /// value: true, + /// onChanged: (_){}, + /// fillColor: MaterialStateProperty.resolveWith((Set states) { + /// if (states.contains(MaterialState.disabled)) { + /// return Colors.orange.withOpacity(.32); + /// } + /// return Colors.orange; + /// }) + /// ) + /// ``` + /// {@end-tool} + /// {@endtemplate} + /// + /// If null, then the value of [activeColor] is used in the selected + /// state. If that is also null, the value of [CheckboxThemeData.fillColor] + /// is used. If that is also null, then [ThemeData.disabledColor] is used in + /// the disabled state, [ThemeData.toggleableActiveColor] is used in the + /// selected state, and [ThemeData.unselectedWidgetColor] is used in the + /// default state. + final MaterialStateProperty? fillColor; + + /// {@template flutter.material.checkbox.checkColor} + /// The color to use for the check icon when this checkbox is checked. + /// {@endtemplate} + /// + /// If null, then the value of [CheckboxThemeData.checkColor] is used. If + /// that is also null, then Color(0xFFFFFFFF) is used. + final Color? checkColor; + + /// If true the checkbox's [value] can be true, false, or null. + /// + /// Checkbox displays a dash when its value is null. + /// + /// When a tri-state checkbox ([tristate] is true) is tapped, its [onChanged] + /// callback will be applied to true if the current value is false, to null if + /// value is true, and to false if value is null (i.e. it cycles through false + /// => true => null => false when tapped). + /// + /// If tristate is false (the default), [value] must not be null. + final bool tristate; + + /// {@template flutter.material.checkbox.materialTapTargetSize} + /// Configures the minimum size of the tap target. + /// {@endtemplate} + /// + /// If null, then the value of [CheckboxThemeData.materialTapTargetSize] is + /// used. If that is also null, then the value of + /// [ThemeData.materialTapTargetSize] is used. + /// + /// See also: + /// + /// * [MaterialTapTargetSize], for a description of how this affects tap targets. + + final MaterialTapTargetSize? materialTapTargetSize; + + /// {@template flutter.material.checkbox.visualDensity} + /// Defines how compact the checkbox's layout will be. + /// {@endtemplate} + /// + /// {@macro flutter.material.themedata.visualDensity} + /// + /// If null, then the value of [CheckboxThemeData.visualDensity] is used. If + /// that is also null, then the value of [ThemeData.visualDensity] is used. + /// + /// See also: + /// + /// * [ThemeData.visualDensity], which specifies the [visualDensity] for all + /// widgets within a [Theme]. + final VisualDensity? visualDensity; + + /// The color for the checkbox's [Material] when it has the input focus. + /// + /// If [overlayColor] returns a non-null color in the [MaterialState.focused] + /// state, it will be used instead. + /// + /// If null, then the value of [CheckboxThemeData.overlayColor] is used in the + /// focused state. If that is also null, then the value of + /// [ThemeData.focusColor] is used. + final Color? focusColor; + + /// The color for the checkbox's [Material] when a pointer is hovering over it. + /// + /// If [overlayColor] returns a non-null color in the [MaterialState.hovered] + /// state, it will be used instead. + /// + /// If null, then the value of [CheckboxThemeData.overlayColor] is used in the + /// hovered state. If that is also null, then the value of + /// [ThemeData.hoverColor] is used. + final Color? hoverColor; + + /// {@template flutter.material.checkbox.overlayColor} + /// The color for the checkbox's [Material]. + /// + /// Resolves in the following states: + /// * [MaterialState.pressed]. + /// * [MaterialState.selected]. + /// * [MaterialState.hovered]. + /// * [MaterialState.focused]. + /// {@endtemplate} + /// + /// If null, then the value of [activeColor] with alpha + /// [kRadialReactionAlpha], [focusColor] and [hoverColor] is used in the + /// pressed, focused and hovered state. If that is also null, + /// the value of [CheckboxThemeData.overlayColor] is used. If that is + /// also null, then the value of [ThemeData.toggleableActiveColor] with alpha + /// [kRadialReactionAlpha], [ThemeData.focusColor] and [ThemeData.hoverColor] + /// is used in the pressed, focused and hovered state. + final MaterialStateProperty? overlayColor; + + /// {@template flutter.material.checkbox.splashRadius} + /// The splash radius of the circular [Material] ink response. + /// {@endtemplate} + /// + /// If null, then the value of [CheckboxThemeData.splashRadius] is used. If + /// that is also null, then [kRadialReactionRadius] is used. + final double? splashRadius; + + /// {@macro flutter.widgets.Focus.focusNode} + final FocusNode? focusNode; + + /// {@macro flutter.widgets.Focus.autofocus} + final bool autofocus; + + ///Controls the borderRadius of the iconbox + final BorderRadius? borderRadius; + + ///Default Flutter Icons are supported for now + ///Icon when the checkbox is in activestate + final IconData? activeIcon; + + ///Default Flutter Icons are supported for now + ///Icon when the checkbox is in inactivestate + final IconData? inactiveIcon; + + ///Default Flutter Icons are supported for now + ///Icon when the checkbox is in triactivestate + final IconData? tristateIcon; + + ///Will support in future + // final OutlinedBorder? shape; + ///Will support in future + // final BorderSide? side; + + static const double width = 18.0; + @override + State createState() => _CustomIconedCheckboxState(); +} + +class _CustomIconedCheckboxState extends State + with TickerProviderStateMixin { + // bool? _previousValue; + Set get states => { + if (!isInteractive) MaterialState.disabled, + if (_hovering) MaterialState.hovered, + if (_focused) MaterialState.focused, + if (value ?? true) MaterialState.selected, + }; + late final Map> _actionMap = >{ + ActivateIntent: CallbackAction(onInvoke: _handleTap), + }; + @override + void initState() { + super.initState(); + // _previousValue = widget.value; + } + + @override + void didUpdateWidget(CustomIconedCheckbox oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.value != widget.value) { + // _previousValue = oldWidget.value; + // animateToValue(); + } + } + + @override + void dispose() { + super.dispose(); + } + + ValueChanged? get onChanged => widget.onChanged; + bool get tristate => widget.tristate; + bool? get value => widget.value; + + MaterialStateProperty get _widgetFillColor { + return MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return null; + } + if (states.contains(MaterialState.selected)) { + return widget.activeColor; + } + return null; + }); + } + + MaterialStateProperty get _defaultFillColor { + final ThemeData themeData = Theme.of(context); + return MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return themeData.disabledColor; + } + if (states.contains(MaterialState.selected)) { + return themeData.toggleableActiveColor; + } + return themeData.unselectedWidgetColor; + }); + } + + BorderSide? _resolveSide(BorderSide? side) { + if (side is MaterialStateBorderSide) { + return MaterialStateProperty.resolveAs(side, states); + } + if (!states.contains(MaterialState.selected)) { + return side; + } + return null; + } + + bool get isInteractive => onChanged != null; + bool _focused = false; + void _handleFocusHighlightChanged(bool focused) { + if (focused != _focused) { + _focused = focused; + setState(() {}); + } + } + + bool _hovering = false; + void _handleHoverEnter(PointerEnterEvent event) { + if (!isInteractive) return; + _downPosition = event.localPosition; + _hovering = true; + setState(() {}); + } + + void _handleHoverExit(PointerExitEvent event) { + if (!isInteractive) return; + _downPosition = event.localPosition; + _hovering = false; + setState(() {}); + } + + void _handleTap([Intent? _]) { + if (!isInteractive) return; + + switch (value) { + case false: + onChanged!(true); + break; + case true: + onChanged!(tristate ? null : false); + break; + case null: + onChanged!(false); + break; + } + context.findRenderObject()!.sendSemanticsEvent(const TapSemanticEvent()); + } + + Offset? get downPosition => _downPosition; + Offset? _downPosition; + + void _handleTapDown(TapDownDetails details) { + if (isInteractive) { + _downPosition = details.localPosition; + setState(() {}); + } + } + + void _handleTapEnd([TapUpDetails? _]) { + if (_downPosition != null) { + _downPosition = null; + setState(() {}); + } + } + + @override + Widget build(BuildContext context) { + assert(debugCheckHasMaterial(context)); + final ThemeData themeData = Theme.of(context); + final CheckboxThemeData checkboxTheme = CheckboxTheme.of(context); + final MaterialTapTargetSize effectiveMaterialTapTargetSize = + widget.materialTapTargetSize ?? + checkboxTheme.materialTapTargetSize ?? + themeData.materialTapTargetSize; + final VisualDensity effectiveVisualDensity = widget.visualDensity ?? + checkboxTheme.visualDensity ?? + themeData.visualDensity; + // Colors need to be resolved in selected and non selected states separately + // so that they can be lerped between. + final Set activeStates = states..add(MaterialState.selected); + final Color effectiveActiveColor = + widget.fillColor?.resolve(activeStates) ?? + _widgetFillColor.resolve(activeStates) ?? + checkboxTheme.fillColor?.resolve(activeStates) ?? + _defaultFillColor.resolve(activeStates); + + final Set focusedStates = states..add(MaterialState.focused); + final Color effectiveFocusOverlayColor = + widget.overlayColor?.resolve(focusedStates) ?? + widget.focusColor ?? + checkboxTheme.overlayColor?.resolve(focusedStates) ?? + themeData.focusColor; + + final Set hoveredStates = states..add(MaterialState.hovered); + final Color effectiveHoverOverlayColor = + widget.overlayColor?.resolve(hoveredStates) ?? + widget.hoverColor ?? + checkboxTheme.overlayColor?.resolve(hoveredStates) ?? + themeData.hoverColor; + + final Color effectiveBorderColor = + widget.fillColor?.resolve(activeStates) ?? + _widgetFillColor.resolve(states) ?? + _defaultFillColor.resolve(states); + final Color effectiveCheckColor = widget.checkColor ?? + checkboxTheme.checkColor?.resolve(states) ?? + const Color(0xFFFFFFFF); + + Size size; + double iconSize; + double marginAdjuster; + double iconSizeAdjuster; + EdgeInsets effectiveMargin; + + switch (effectiveMaterialTapTargetSize) { + case MaterialTapTargetSize.padded: + size = const Size(kMinInteractiveDimension, kMinInteractiveDimension); + marginAdjuster = _kContainerPadddedMarginAdjuster; + iconSizeAdjuster = _kPaddedIconSizeAdjuster; + break; + case MaterialTapTargetSize.shrinkWrap: + size = const Size( + kMinInteractiveDimension - 8.0, kMinInteractiveDimension - 8.0); + marginAdjuster = _kContainerShrinkMarginAdjuster; + iconSizeAdjuster = _kShrinkIconSizeAdjuster; + break; + } + size += effectiveVisualDensity.baseSizeAdjustment; + effectiveMargin = EdgeInsets.all(size.height / marginAdjuster); + iconSize = size.height / iconSizeAdjuster; + + final MaterialStateProperty effectiveMouseCursor = + MaterialStateProperty.resolveWith( + (Set states) { + return MaterialStateProperty.resolveAs( + widget.mouseCursor, states) ?? + checkboxTheme.mouseCursor?.resolve(states) ?? + MaterialStateMouseCursor.clickable.resolve(states); + }); + BorderRadius effectiveBorderRadius = + widget.borderRadius ?? BorderRadius.circular(1.0); + + return Semantics( + checked: widget.value ?? false, + child: FocusableActionDetector( + mouseCursor: effectiveMouseCursor.resolve(states), + autofocus: widget.autofocus, + focusNode: widget.focusNode, + enabled: isInteractive, + onShowFocusHighlight: _handleFocusHighlightChanged, + actions: _actionMap, + child: MouseRegion( + onEnter: _handleHoverEnter, + onExit: _handleHoverExit, + child: GestureDetector( + excludeFromSemantics: !isInteractive, + onTap: _handleTap, + onTapDown: _handleTapDown, + onTapUp: _handleTapEnd, + onTapCancel: _handleTapEnd, + child: CustomPaint( + size: size, + painter: RadialReactionPainter() + ..downPosition = _downPosition + ..splashRadius = widget.splashRadius ?? kRadialReactionRadius + ..hoverColor = effectiveHoverOverlayColor + ..focusColor = effectiveFocusOverlayColor + ..isHovered = _hovering + ..isFocused = _focused, + child: SizedBox.fromSize( + size: size, + child: AnimatedContainer( + duration: const Duration(milliseconds: 400), + margin: effectiveMargin, + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: effectiveBorderRadius, + border: Border.all( + width: 2.0, + color: value == true || value == null + ? Colors.transparent + : effectiveBorderColor, + ), + color: value == true || value == null + ? effectiveActiveColor + : Colors.transparent, + ), + child: getEffectiveIconWidgetStateWidget( + effectiveCheckColor, + size: iconSize, + ), + ), + ), + ), + ), + ), + ), + ); + } + + Widget? getEffectiveIconWidgetStateWidget(Color iconColor, + {double size = 14}) { + late final Widget? effectiveIconWidget; + final effectiveActiveIcon = widget.activeIcon ?? Icons.check; + final effectiveInactiveIcon = widget.inactiveIcon; + final effectiveTristateIcon = widget.tristateIcon ?? Icons.remove; + + if (value == true) { + effectiveIconWidget = _getIconBase( + effectiveActiveIcon, + iconColor, + size, + ); + } else if (value == false) { + if (effectiveInactiveIcon != null) { + effectiveIconWidget = _getIconBase( + effectiveInactiveIcon, + iconColor, + size, + ); + } else { + effectiveIconWidget = null; + } + } else { + //tristate + effectiveIconWidget = _getIconBase( + effectiveTristateIcon, + iconColor, + size, + ); + } + return effectiveIconWidget; + } + + Widget _getIconBase(IconData icon, Color color, double size) { + return Text( + String.fromCharCode(icon.codePoint), + style: TextStyle( + inherit: false, + color: color, + fontSize: size, + fontWeight: FontWeight.bold, + fontFamily: icon.fontFamily, + package: icon.fontPackage, + ), + ); + } +} + +const double _kContainerPadddedMarginAdjuster = 4.0; +const double _kContainerShrinkMarginAdjuster = 4.5; +const double _kPaddedIconSizeAdjuster = 2.8; +const double _kShrinkIconSizeAdjuster = 2.75; diff --git a/lib/src/widgets/custom_iconed_checkbox_list_tile.dart b/lib/src/widgets/custom_iconed_checkbox_list_tile.dart new file mode 100644 index 0000000000..221fbcf7eb --- /dev/null +++ b/lib/src/widgets/custom_iconed_checkbox_list_tile.dart @@ -0,0 +1,253 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_form_builder/src/widgets/custom_iconed_checkbox.dart'; + +class CustomIconedCheckboxListTile extends StatelessWidget { + const CustomIconedCheckboxListTile({ + super.key, + required this.value, + required this.onChanged, + this.activeColor, + this.checkColor, + this.enabled, + this.tileColor, + this.title, + this.subtitle, + this.isThreeLine = false, + this.dense, + this.secondary, + this.selected = false, + this.controlAffinity = ListTileControlAffinity.platform, + this.autofocus = false, + this.contentPadding, + this.tristate = false, + this.shape, + this.borderRadius, + this.selectedTileColor, + this.visualDensity, + this.focusNode, + this.enableFeedback, + this.activeIcon, + this.inactiveIcon, + this.tristateIcon, + }) : assert(tristate || value != null), + assert(!isThreeLine || subtitle != null); + + /// Whether this checkbox is checked. + final bool? value; + + /// Called when the value of the checkbox should change. + /// + /// The checkbox passes the new value to the callback but does not actually + /// change state until the parent widget rebuilds the checkbox tile with the + /// new value. + /// + /// If null, the checkbox will be displayed as disabled. + /// + /// {@tool snippet} + /// + /// The callback provided to [onChanged] should update the state of the parent + /// [StatefulWidget] using the [State.setState] method, so that the parent + /// gets rebuilt; for example: + /// + /// ```dart + /// CheckboxListTile( + /// value: _throwShotAway, + /// onChanged: (bool? newValue) { + /// setState(() { + /// _throwShotAway = newValue; + /// }); + /// }, + /// title: const Text('Throw away your shot'), + /// ) + /// ``` + /// {@end-tool} + final ValueChanged? onChanged; + + /// The color to use when this checkbox is checked. + /// + /// Defaults to accent color of the current [Theme]. + final Color? activeColor; + + /// The color to use for the check icon when this checkbox is checked. + /// + /// Defaults to Color(0xFFFFFFFF). + final Color? checkColor; + + /// {@macro flutter.material.ListTile.tileColor} + final Color? tileColor; + + /// The primary content of the list tile. + /// + /// Typically a [Text] widget. + final Widget? title; + + /// Additional content displayed below the title. + /// + /// Typically a [Text] widget. + final Widget? subtitle; + + /// A widget to display on the opposite side of the tile from the checkbox. + /// + /// Typically an [Icon] widget. + final Widget? secondary; + + /// Whether this list tile is intended to display three lines of text. + /// + /// If false, the list tile is treated as having one line if the subtitle is + /// null and treated as having two lines if the subtitle is non-null. + final bool isThreeLine; + + /// Whether this list tile is part of a vertically dense list. + /// + /// If this property is null then its value is based on [ListTileThemeData.dense]. + final bool? dense; + + /// Whether to render icons and text in the [activeColor]. + /// + /// No effort is made to automatically coordinate the [selected] state and the + /// [value] state. To have the list tile appear selected when the checkbox is + /// checked, pass the same value to both. + /// + /// Normally, this property is left to its default value, false. + final bool selected; + + /// Where to place the control relative to the text. + final ListTileControlAffinity controlAffinity; + + /// {@macro flutter.widgets.Focus.autofocus} + final bool autofocus; + + /// Defines insets surrounding the tile's contents. + /// + /// This value will surround the [Checkbox], [title], [subtitle], and [secondary] + /// widgets in [CheckboxListTile]. + /// + /// When the value is null, the `contentPadding` is `EdgeInsets.symmetric(horizontal: 16.0)`. + final EdgeInsetsGeometry? contentPadding; + + /// If true the checkbox's [value] can be true, false, or null. + /// + /// Checkbox displays a dash when its value is null. + /// + /// When a tri-state checkbox ([tristate] is true) is tapped, its [onChanged] + /// callback will be applied to true if the current value is false, to null if + /// value is true, and to false if value is null (i.e. it cycles through false + /// => true => null => false when tapped). + /// + /// If tristate is false (the default), [value] must not be null. + final bool tristate; + + /// {@macro flutter.material.ListTile.shape} + final ShapeBorder? shape; + + /// {@macro flutter.material.checkbox.shape} + /// + /// If this property is null then [CheckboxThemeData.shape] of [ThemeData.checkboxTheme] + /// is used. If that's null then the shape will be a [RoundedRectangleBorder] + /// with a circular corner radius of 1.0. + final BorderRadius? borderRadius; + + /// If non-null, defines the background color when [CheckboxListTile.selected] is true. + final Color? selectedTileColor; + + /// Defines how compact the list tile's layout will be. + /// + /// {@macro flutter.material.themedata.visualDensity} + final VisualDensity? visualDensity; + + /// {@macro flutter.widgets.Focus.focusNode} + final FocusNode? focusNode; + + /// {@macro flutter.material.ListTile.enableFeedback} + /// + /// See also: + /// + /// * [Feedback] for providing platform-specific feedback to certain actions. + final bool? enableFeedback; + + ///Default Flutter Icons are supported for now + ///Icon when the checkbox is in activestate + final IconData? activeIcon; + + ///Default Flutter Icons are supported for now + ///Icon when the checkbox is in inactivestate + final IconData? inactiveIcon; + + ///Default Flutter Icons are supported for now + ///Icon when the checkbox is in triactivestate + final IconData? tristateIcon; + + /// Whether the CheckboxListTile is interactive. + /// + /// If false, this list tile is styled with the disabled color from the + /// current [Theme] and the [ListTile.onTap] callback is + /// inoperative. + final bool? enabled; + void _handleValueChange() { + assert(onChanged != null); + switch (value) { + case false: + onChanged!(true); + break; + case true: + onChanged!(tristate ? null : false); + break; + case null: + onChanged!(false); + break; + } + } + + @override + Widget build(BuildContext context) { + final Widget control = CustomIconedCheckbox( + value: value, + onChanged: enabled ?? true ? onChanged : null, + activeColor: activeColor, + checkColor: checkColor, + materialTapTargetSize: MaterialTapTargetSize.padded, + autofocus: autofocus, + tristate: tristate, + borderRadius: borderRadius, + activeIcon: activeIcon, + inactiveIcon: inactiveIcon, + tristateIcon: tristateIcon, + ); + Widget? leading, trailing; + switch (controlAffinity) { + case ListTileControlAffinity.leading: + leading = control; + trailing = secondary; + break; + case ListTileControlAffinity.trailing: + case ListTileControlAffinity.platform: + leading = secondary; + trailing = control; + break; + } + return MergeSemantics( + child: ListTileTheme.merge( + selectedColor: activeColor ?? Theme.of(context).toggleableActiveColor, + child: ListTile( + leading: leading, + title: title, + subtitle: subtitle, + trailing: trailing, + isThreeLine: isThreeLine, + dense: dense, + enabled: enabled ?? onChanged != null, + onTap: onChanged != null ? _handleValueChange : null, + selected: selected, + autofocus: autofocus, + contentPadding: contentPadding, + shape: shape, + selectedTileColor: selectedTileColor, + tileColor: tileColor, + visualDensity: visualDensity, + focusNode: focusNode, + enableFeedback: enableFeedback, + ), + ), + ); + } +} diff --git a/lib/src/widgets/grouped_checkbox.dart b/lib/src/widgets/grouped_checkbox.dart index 3cc65edb5a..781598c583 100644 --- a/lib/src/widgets/grouped_checkbox.dart +++ b/lib/src/widgets/grouped_checkbox.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:flutter_form_builder/src/widgets/custom_iconed_checkbox.dart'; class GroupedCheckbox extends StatelessWidget { /// A list of string that describes each checkbox. Each item must be distinct. @@ -180,6 +181,20 @@ class GroupedCheckbox extends StatelessWidget { final ControlAffinity controlAffinity; + ///Controls the borderRadius of the iconbox + final BorderRadius? borderRadius; + + ///Default Flutter Icons are supported for now + ///Icon when the checkbox is in activestate + final IconData? activeIcon; + + ///Default Flutter Icons are supported for now + ///Icon when the checkbox is in inactivestate + final IconData? inactiveIcon; + + ///Default Flutter Icons are supported for now + ///Icon when the checkbox is in triactivestate + final IconData? tristateIcon; const GroupedCheckbox({ Key? key, required this.options, @@ -203,6 +218,10 @@ class GroupedCheckbox extends StatelessWidget { this.wrapVerticalDirection = VerticalDirection.down, this.separator, this.controlAffinity = ControlAffinity.leading, + this.activeIcon, + this.inactiveIcon, + this.tristateIcon, + this.borderRadius, }) : super(key: key); @override @@ -251,7 +270,7 @@ class GroupedCheckbox extends StatelessWidget { final option = options[index]; final optionValue = option.value; final isOptionDisabled = true == disabled?.contains(optionValue); - final control = Checkbox( + final control = CustomIconedCheckbox( activeColor: activeColor, checkColor: checkColor, focusColor: focusColor, @@ -261,6 +280,10 @@ class GroupedCheckbox extends StatelessWidget { ? value?.contains(optionValue) : true == value?.contains(optionValue), tristate: tristate, + activeIcon: activeIcon, + inactiveIcon: inactiveIcon, + tristateIcon: tristateIcon, + borderRadius: borderRadius, onChanged: isOptionDisabled ? null : (selected) { diff --git a/pubspec.lock b/pubspec.lock index 6ba1a68aaf..a4dd5de741 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -7,7 +7,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.9.0" + version: "2.8.2" boolean_selector: dependency: transitive description: @@ -21,14 +21,21 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.2.1" + version: "1.2.0" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.1" clock: dependency: transitive description: name: clock url: "https://pub.dartlang.org" source: hosted - version: "1.1.1" + version: "1.1.0" collection: dependency: "direct main" description: @@ -42,7 +49,7 @@ packages: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.3.1" + version: "1.3.0" flutter: dependency: "direct main" description: flutter @@ -80,28 +87,28 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.12" + version: "0.12.11" material_color_utilities: dependency: transitive description: name: material_color_utilities url: "https://pub.dartlang.org" source: hosted - version: "0.1.5" + version: "0.1.4" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "1.7.0" path: dependency: transitive description: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.2" + version: "1.8.1" sky_engine: dependency: transitive description: flutter @@ -113,7 +120,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.9.0" + version: "1.8.2" stack_trace: dependency: transitive description: @@ -134,21 +141,21 @@ packages: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.1.1" + version: "1.1.0" term_glyph: dependency: transitive description: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.2.1" + version: "1.2.0" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.12" + version: "0.4.9" vector_math: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 64de9076f3..2103751a1b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_form_builder description: This package helps in creation of forms in Flutter by removing the boilerplate code, reusing validation, react to changes, and collect final user input. -version: 7.7.0 +version: 7.7.1 repository: https://github.com/flutter-form-builder-ecosystem/flutter_form_builder issue_tracker: https://github.com/flutter-form-builder-ecosystem/flutter_form_builder/issues