Instantly share code, notes, and snippets.
Created
December 20, 2021 10:57
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
Save gladson/965ca797cf6eb89cd39cf7fa5a8afa4b to your computer and use it in GitHub Desktop.
Custom Radio Button - Flutter
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:flutter/material.dart'; | |
const double _kEdgeSize = Checkbox.width * 1.5; | |
const double _kStrokeWidth = 0.0; | |
class SRadio<T> extends StatefulWidget { | |
const SRadio({ | |
Key? key, | |
required this.value, | |
required this.groupValue, | |
required this.onChanged, | |
this.mouseCursor, | |
this.toggleable = false, | |
this.activeColor, | |
this.fillColor, | |
this.checkColor, | |
this.focusColor, | |
this.hoverColor, | |
this.overlayColor, | |
this.splashRadius, | |
this.materialTapTargetSize, | |
this.visualDensity, | |
this.focusNode, | |
this.autofocus = false, | |
this.shape, | |
this.side, | |
}) : super(key: key); | |
final T value; | |
final T? groupValue; | |
final ValueChanged<T?>? onChanged; | |
final MouseCursor? mouseCursor; | |
final Color? checkColor; | |
final bool toggleable; | |
final Color? activeColor; | |
final MaterialStateProperty<Color?>? fillColor; | |
final MaterialTapTargetSize? materialTapTargetSize; | |
final VisualDensity? visualDensity; | |
final Color? focusColor; | |
final Color? hoverColor; | |
final MaterialStateProperty<Color?>? overlayColor; | |
final double? splashRadius; | |
final FocusNode? focusNode; | |
final bool autofocus; | |
final OutlinedBorder? shape; | |
final BorderSide? side; | |
bool get _selected => value == groupValue; | |
@override | |
State<SRadio<T>> createState() => _SRadioState<T>(); | |
} | |
class _SRadioState<T> extends State<SRadio<T>> | |
with TickerProviderStateMixin, ToggleableStateMixin { | |
final _SRadioPainter _painter = _SRadioPainter(); | |
bool? _previousValue; | |
void _handleChanged(bool? selected) { | |
if (selected == null) { | |
widget.onChanged!(null); | |
return; | |
} | |
if (selected) { | |
widget.onChanged!(widget.value); | |
} | |
} | |
@override | |
void didUpdateWidget(SRadio<T> oldWidget) { | |
super.didUpdateWidget(oldWidget); | |
if (widget._selected != oldWidget._selected) { | |
animateToValue(); | |
} | |
} | |
@override | |
void dispose() { | |
_painter.dispose(); | |
super.dispose(); | |
} | |
@override | |
ValueChanged<bool?>? get onChanged => | |
widget.onChanged != null ? _handleChanged : null; | |
@override | |
bool get tristate => widget.toggleable; | |
@override | |
bool? get value => widget._selected; | |
MaterialStateProperty<Color?> get _widgetFillColor { | |
return MaterialStateProperty.resolveWith((Set<MaterialState> states) { | |
if (states.contains(MaterialState.disabled)) { | |
return null; | |
} | |
if (states.contains(MaterialState.selected)) { | |
return widget.activeColor; | |
} | |
return null; | |
}); | |
} | |
MaterialStateProperty<Color> get _defaultFillColor { | |
final ThemeData themeData = Theme.of(context); | |
return MaterialStateProperty.resolveWith((Set<MaterialState> 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<BorderSide?>(side, states); | |
} | |
if (!states.contains(MaterialState.selected)) { | |
return side; | |
} | |
return null; | |
} | |
@override | |
Widget build(BuildContext context) { | |
assert(debugCheckHasMaterial(context)); | |
final ThemeData themeData = Theme.of(context); | |
final MaterialTapTargetSize effectiveMaterialTapTargetSize = | |
widget.materialTapTargetSize ?? | |
themeData.radioTheme.materialTapTargetSize ?? | |
themeData.materialTapTargetSize; | |
final VisualDensity effectiveVisualDensity = widget.visualDensity ?? | |
themeData.radioTheme.visualDensity ?? | |
themeData.visualDensity; | |
Size size; | |
switch (effectiveMaterialTapTargetSize) { | |
case MaterialTapTargetSize.padded: | |
size = const Size(kMinInteractiveDimension, kMinInteractiveDimension); | |
break; | |
case MaterialTapTargetSize.shrinkWrap: | |
size = const Size( | |
kMinInteractiveDimension - 8.0, kMinInteractiveDimension - 8.0); | |
break; | |
} | |
size += effectiveVisualDensity.baseSizeAdjustment; | |
final MaterialStateProperty<MouseCursor> effectiveMouseCursor = | |
MaterialStateProperty.resolveWith<MouseCursor>( | |
(Set<MaterialState> states) { | |
return MaterialStateProperty.resolveAs<MouseCursor?>( | |
widget.mouseCursor, states) ?? | |
themeData.radioTheme.mouseCursor?.resolve(states) ?? | |
MaterialStateProperty.resolveAs<MouseCursor>( | |
MaterialStateMouseCursor.clickable, states); | |
}); | |
final Set<MaterialState> activeStates = states..add(MaterialState.selected); | |
final Set<MaterialState> inactiveStates = states | |
..remove(MaterialState.selected); | |
final Color effectiveActiveColor = | |
widget.fillColor?.resolve(activeStates) ?? | |
_widgetFillColor.resolve(activeStates) ?? | |
themeData.radioTheme.fillColor?.resolve(activeStates) ?? | |
_defaultFillColor.resolve(activeStates); | |
final Color effectiveInactiveColor = | |
widget.fillColor?.resolve(inactiveStates) ?? | |
_widgetFillColor.resolve(inactiveStates) ?? | |
themeData.radioTheme.fillColor?.resolve(inactiveStates) ?? | |
_defaultFillColor.resolve(inactiveStates); | |
final Set<MaterialState> focusedStates = states..add(MaterialState.focused); | |
final Color effectiveFocusOverlayColor = | |
widget.overlayColor?.resolve(focusedStates) ?? | |
widget.focusColor ?? | |
themeData.radioTheme.overlayColor?.resolve(focusedStates) ?? | |
themeData.focusColor; | |
final Set<MaterialState> hoveredStates = states..add(MaterialState.hovered); | |
final Color effectiveHoverOverlayColor = | |
widget.overlayColor?.resolve(hoveredStates) ?? | |
widget.hoverColor ?? | |
themeData.radioTheme.overlayColor?.resolve(hoveredStates) ?? | |
themeData.hoverColor; | |
final Set<MaterialState> activePressedStates = activeStates | |
..add(MaterialState.pressed); | |
final Color effectiveActivePressedOverlayColor = | |
widget.overlayColor?.resolve(activePressedStates) ?? | |
themeData.radioTheme.overlayColor?.resolve(activePressedStates) ?? | |
effectiveActiveColor.withAlpha(kRadialReactionAlpha); | |
final Set<MaterialState> inactivePressedStates = inactiveStates | |
..add(MaterialState.pressed); | |
final Color effectiveInactivePressedOverlayColor = | |
widget.overlayColor?.resolve(inactivePressedStates) ?? | |
themeData.radioTheme.overlayColor?.resolve(inactivePressedStates) ?? | |
effectiveActiveColor.withAlpha(kRadialReactionAlpha); | |
final Color effectiveCheckColor = widget.checkColor ?? | |
themeData.checkboxTheme.checkColor?.resolve(states) ?? | |
const Color(0xFFFFFFFF); | |
return Semantics( | |
inMutuallyExclusiveGroup: true, | |
checked: widget._selected, | |
child: buildToggleable( | |
focusNode: widget.focusNode, | |
autofocus: widget.autofocus, | |
mouseCursor: effectiveMouseCursor, | |
size: size, | |
painter: _painter | |
..position = position | |
..reaction = reaction | |
..reactionFocusFade = reactionFocusFade | |
..reactionHoverFade = reactionHoverFade | |
..inactiveReactionColor = effectiveInactivePressedOverlayColor | |
..reactionColor = effectiveActivePressedOverlayColor | |
..hoverColor = effectiveHoverOverlayColor | |
..focusColor = effectiveFocusOverlayColor | |
..splashRadius = widget.splashRadius ?? | |
themeData.radioTheme.splashRadius ?? | |
kRadialReactionRadius | |
..downPosition = downPosition | |
..isFocused = states.contains(MaterialState.focused) | |
..isHovered = states.contains(MaterialState.hovered) | |
..activeColor = effectiveActiveColor | |
..inactiveColor = effectiveInactiveColor | |
..checkColor = effectiveCheckColor | |
..value = value | |
..previousValue = _previousValue | |
..shape = widget.shape ?? | |
themeData.checkboxTheme.shape ?? | |
const RoundedRectangleBorder( | |
borderRadius: BorderRadius.all(Radius.circular(1.0)), | |
) | |
..side = _resolveSide(widget.side) ?? | |
_resolveSide(themeData.checkboxTheme.side), | |
), | |
); | |
} | |
} | |
class _SRadioPainter extends ToggleablePainter { | |
Color get checkColor => _checkColor!; | |
Color? _checkColor; | |
set checkColor(Color value) { | |
if (_checkColor == value) { | |
return; | |
} | |
_checkColor = value; | |
notifyListeners(); | |
} | |
bool? get value => _value; | |
bool? _value; | |
set value(bool? value) { | |
if (_value == value) { | |
return; | |
} | |
_value = value; | |
notifyListeners(); | |
} | |
bool? get previousValue => _previousValue; | |
bool? _previousValue; | |
set previousValue(bool? value) { | |
if (_previousValue == value) { | |
return; | |
} | |
_previousValue = value; | |
notifyListeners(); | |
} | |
Rect _outerRectAt(Offset origin, double t) { | |
final double inset = 1.0 - (t - 0.5).abs() * 2.0; | |
final double size = _kEdgeSize - inset * _kStrokeWidth; | |
final Rect rect = Rect.fromLTWH(origin.dx, origin.dy, size, size); | |
return rect; | |
} | |
OutlinedBorder get shape => _shape!; | |
OutlinedBorder? _shape; | |
set shape(OutlinedBorder value) { | |
if (_shape == value) { | |
return; | |
} | |
_shape = value; | |
notifyListeners(); | |
} | |
BorderSide? get side => _side; | |
BorderSide? _side; | |
set side(BorderSide? value) { | |
if (_side == value) { | |
return; | |
} | |
_side = value; | |
notifyListeners(); | |
} | |
void _drawBox( | |
Canvas canvas, Rect outer, Paint paint, BorderSide? side, bool fill) { | |
if (fill) { | |
var size = const Size(30, 30); | |
final Offset origin = | |
size - const Size.square(_kEdgeSize) / 1.7 as Offset; | |
final shapeBounds = Rect.fromLTWH(8, 0, size.width, size.height); | |
final backgroundPath = Path(); | |
backgroundPath | |
..moveTo(shapeBounds.left, shapeBounds.top) | |
..addRect(outer) | |
..close(); | |
canvas.drawPath(backgroundPath, paint); | |
var paint1 = paint | |
..color = _checkColor! | |
..style = PaintingStyle.fill; | |
canvas.drawRect(origin & const Size(20, 20), paint1); | |
} | |
if (side != null) { | |
shape.copyWith(side: side).paint(canvas, outer); | |
} | |
} | |
Color _colorAt(double t) { | |
return t >= 0.25 | |
? activeColor | |
: Color.lerp(inactiveColor, activeColor, t * 4.0)!; | |
} | |
@override | |
void paint(Canvas canvas, Size size) { | |
paintRadialReaction(canvas: canvas, origin: size.center(Offset.zero)); | |
final Offset origin = | |
size / 2.0 - const Size.square(_kEdgeSize) / 2.0 as Offset; | |
final AnimationStatus status = position.status; | |
final double tNormalized = | |
status == AnimationStatus.forward || status == AnimationStatus.completed | |
? position.value | |
: 1.0 - position.value; | |
if (previousValue == false || value == false) { | |
double t = value == false ? 1.0 - tNormalized : tNormalized; | |
final Rect outer = _outerRectAt(origin, t); | |
final Paint paint = Paint()..color = _colorAt(t); | |
if (t <= 0.5) { | |
final BorderSide border = | |
side ?? BorderSide(width: 2, color: paint.color); | |
_drawBox(canvas, outer, paint, border, false); | |
} else { | |
_drawBox(canvas, outer, paint, side, true); | |
} | |
} else { | |
final Rect outer = _outerRectAt(origin, 8.0); | |
final Paint paint = Paint()..color = _colorAt(1.0); | |
paint | |
..style = PaintingStyle.stroke | |
..strokeWidth = 2.0; | |
_drawBox(canvas, outer, paint, side, true); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Para usar no projeto: