最近闲来无事学习flutter模仿开发一个应用用来练手, 准备将学习过程记录成一系列文章备忘, 本文为该系列第(4)篇,全部代码已上传: github 本篇主要记录如何自定义checkbox
Checkbox
flutter 自带checkbox空间. 但自带的checkbox并不好用. 比如外形固定是方框,值变动了并不会重新绘制效果.所以就自定义一个checkbox
首先将checkbox代码复制一份到新文件,重新命名新的checkbox为CustomerCheckBox
checkbox值变动重绘
做法是在_CustomerCheckboxState中加一个记录当前值的变量.[初始设置value是final不能变的].. 并在build中传递的value为当前值.. 然后onChanged传递一个默认的回调函数.在函数中更新界面, 但这里有一个问题时State的生命周期和StatefulWidget是不一样的,这会导致某些情况StatefulWidget的默认值变了而State中的_value并没有不变..所以这里要在didUpdateWidget方法中将_value的值改成新的widget的值
bool _value; //变动值
@override
void didUpdateWidget(CustomerCheckbox oldWidget) {
super.didUpdateWidget(oldWidget);
_value = widget.value;
}
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context));
final ThemeData themeData = Theme.of(context);
Size size = Size(widget.width * 2, widget.width * 2);
final BoxConstraints additionalConstraints = BoxConstraints.tight(size);
return _CheckboxRenderObjectWidget(
width: widget.width,
value: _value ?? widget.value, //当前值
isCircle: widget.isCircle,
tristate: widget.tristate,
activeColor: widget.activeColor ?? themeData.toggleableActiveColor,
checkColor: widget.checkColor ?? const Color(0xFFFFFFFF),
inactiveColor: widget.onChanged != null
? themeData.unselectedWidgetColor
: themeData.disabledColor,
onChanged: (val) { //默认回调函数
_value = val;
if (mounted) {
setState(() {}); //更新界面
}
if (widget.onChanged != null) {
widget.onChanged(val); // 调用原本的回调函数
}
},
additionalConstraints: additionalConstraints,
vsync: this,
);
}
}
checkbox圆形外观
默认的checkbox是方框.. 但是很多时候会有需要圆形外观的需求.. checkbox圆形选项按钮需要在绘制的时候控制.. 增加一个设置圆形外观的参数并在绘图时绘制圆形即可, 主要是在_RenderCheckbox的_outerRectAt函数里这个函数是用来设置绘制的区域的. 所以在需要绘制圆形的话只需要返回圆形区域即可. 代码如下
final bool isCircle; //增加一个参数控制是否绘制圆形
.... // 将参数一直传递到_RenderCheckbox中
// ---- 下面是_RenderCheckbox的_outerRectAt代码 ----
// The square outer bounds of the checkbox at t, with the specified origin.
// At t == 0.0, the outer rect's size is _kEdgeSize (Checkbox.width)
// At t == 0.5, .. is _kEdgeSize - _kStrokeWidth
// At t == 1.0, .. is _kEdgeSize
RRect _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 + inset, origin.dy + inset, size, size);
if (_isCircle) { // 绘制圆形, 返回圆形区域
return RRect.fromRectAndRadius(rect, Radius.circular(_kEdgeSize));
}
return RRect.fromRectAndRadius(rect, _kEdgeRadius);
}
checkbox大小
自带checkbox是不能设置大小的,默认是18.[在checkbox.dart在134行],大小固定后在小屏幕会导致整体突出和其他不和谐所以会有修改checkbox大小的需求..
// checkbox.dart 134行
/// The width of a checkbox widget.
static const double width = 18.0;
既然知道代码是在checkbox.dart在134行控制的,那只要将这个固定的参数
改成初始化时指定,然后在build函数中调整尺寸按传递的参数即可
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context));
final ThemeData themeData = Theme.of(context);
Size size = Size(widget.width * 2, widget.width * 2); //调整
final BoxConstraints additionalConstraints = BoxConstraints.tight(size);
return _CheckboxRenderObjectWidget(
width: widget.width,
value: _value,
isCircle: widget.isCircle,
tristate: widget.tristate,
activeColor: widget.activeColor ?? themeData.toggleableActiveColor,
checkColor: widget.checkColor ?? const Color(0xFFFFFFFF),
inactiveColor: widget.onChanged != null
? themeData.unselectedWidgetColor
: themeData.disabledColor,
onChanged: (val) {
_value = val;
if (mounted) {
setState(() {});
}
if (widget.onChanged != null) {
widget.onChanged(val);
}
},
additionalConstraints: additionalConstraints,
vsync: this,
);
}
以下是完整代码
/// 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.
///
/// See also:
///
/// * [CheckboxListTile], which combines this widget with a [ListTile] so that
/// you can give the checkbox a label.
/// * [Switch], a widget with semantics similar to [Checkbox].
/// * [Radio], for selecting among a set of explicit values.
/// * [Slider], for selecting a value in a range.
/// *
/// *
class CustomerCheckbox extends StatefulWidget {
/// Creates 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 following arguments are required:
///
/// * [value], which determines whether the checkbox is checked. The [value]
/// can only be null if [tristate] is true.
/// * [onChanged], which is called when the value of the checkbox should
/// change. It can be set to null to disable the checkbox.
///
/// The value of [tristate] must not be null.
const CustomerCheckbox({
Key key,
@required this.value,
this.tristate = false,
@required this.onChanged,
this.activeColor,
this.width = 18.0,
this.checkColor,
this.isCircle = false,
}) : assert(tristate != null),
assert(tristate || value != null),
super(key: key);
/// 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;
/// The color to use when this checkbox is checked.
///
/// Defaults to [ThemeData.toggleableActiveColor].
final Color activeColor;
/// The color to use for the check icon when this checkbox is checked
///
/// Defaults to Color(0xFFFFFFFF)
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 is tapped its [onChanged] callback will be
/// applied to true if the current value is null or false, false otherwise.
/// Typically tri-state checkboxes are disabled (the onChanged callback is
/// null) so they don't respond to taps.
///
/// If tristate is false (the default), [value] must not be null.
final bool tristate;
/// The width of a checkbox widget.
final double width;
final bool isCircle; //是否绘制圆形
@override
_CustomerCheckboxState createState() => _CustomerCheckboxState();
}
class _CustomerCheckboxState extends State
with TickerProviderStateMixin {
bool _value;
@override
void didUpdateWidget(CustomerCheckbox oldWidget) {
super.didUpdateWidget(oldWidget);
_value = widget.value;
}
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context));
final ThemeData themeData = Theme.of(context);
Size size = Size(widget.width * 2, widget.width * 2);
final BoxConstraints additionalConstraints = BoxConstraints.tight(size);
return _CheckboxRenderObjectWidget(
width: widget.width,
value: _value ?? widget.value,
isCircle: widget.isCircle,
tristate: widget.tristate,
activeColor: widget.activeColor ?? themeData.toggleableActiveColor,
checkColor: widget.checkColor ?? const Color(0xFFFFFFFF),
inactiveColor: widget.onChanged != null
? themeData.unselectedWidgetColor
: themeData.disabledColor,
onChanged: (val) {
_value = val;
if (mounted) {
setState(() {});
}
if (widget.onChanged != null) {
widget.onChanged(val);
}
},
additionalConstraints: additionalConstraints,
vsync: this,
);
}
}
class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget {
const _CheckboxRenderObjectWidget({
Key key,
@required this.value,
@required this.tristate,
@required this.activeColor,
@required this.checkColor,
@required this.inactiveColor,
@required this.onChanged,
@required this.width,
@required this.vsync,
@required this.isCircle,
@required this.additionalConstraints,
}) : assert(tristate != null),
assert(tristate || value != null),
assert(activeColor != null),
assert(inactiveColor != null),
assert(vsync != null),
super(key: key);
final bool value;
final bool tristate;
final Color activeColor;
final Color checkColor;
final Color inactiveColor;
final double width;
final ValueChanged onChanged;
final TickerProvider vsync;
final BoxConstraints additionalConstraints;
final bool isCircle;
@override
_RenderCheckbox createRenderObject(BuildContext context) => _RenderCheckbox(
value: value,
tristate: tristate,
activeColor: activeColor,
checkColor: checkColor,
inactiveColor: inactiveColor,
onChanged: onChanged,
vsync: vsync,
width: width,
isCircle: isCircle,
additionalConstraints: additionalConstraints,
);
@override
void updateRenderObject(BuildContext context, _RenderCheckbox renderObject) {
renderObject
..value = value
..tristate = tristate
..activeColor = activeColor
..checkColor = checkColor
..inactiveColor = inactiveColor
..onChanged = onChanged
.._isCircle = isCircle
..additionalConstraints = additionalConstraints
..vsync = vsync;
}
}
const Radius _kEdgeRadius = Radius.circular(1.0);
const double _kStrokeWidth = 2.0;
class _RenderCheckbox extends RenderToggleable {
_RenderCheckbox({
bool value,
bool tristate,
Color activeColor,
this.checkColor,
bool isCircle,
Color inactiveColor,
BoxConstraints additionalConstraints,
ValueChanged onChanged,
double width,
@required TickerProvider vsync,
}) : _oldValue = value,
_kEdgeSize = width,
_isCircle = isCircle,
super(
value: value,
tristate: tristate,
activeColor: activeColor,
inactiveColor: inactiveColor,
onChanged: onChanged,
additionalConstraints: additionalConstraints,
vsync: vsync,
);
bool _oldValue;
Color checkColor;
double _kEdgeSize;
bool _isCircle;
@override
set value(bool newValue) {
if (newValue == value) return;
_oldValue = value;
super.value = newValue;
}
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
config.isChecked = value == true;
}
// The square outer bounds of the checkbox at t, with the specified origin.
// At t == 0.0, the outer rect's size is _kEdgeSize (Checkbox.width)
// At t == 0.5, .. is _kEdgeSize - _kStrokeWidth
// At t == 1.0, .. is _kEdgeSize
RRect _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 + inset, origin.dy + inset, size, size);
if (_isCircle) {
return RRect.fromRectAndRadius(rect, Radius.circular(_kEdgeSize));
}
return RRect.fromRectAndRadius(rect, _kEdgeRadius);
}
// The checkbox's border color if value == false, or its fill color when
// value == true or null.
Color _colorAt(double t) {
// As t goes from 0.0 to 0.25, animate from the inactiveColor to activeColor.
return onChanged == null
? inactiveColor
: (t >= 0.25
? activeColor
: Color.lerp(inactiveColor, activeColor, t * 4.0));
}
// White stroke used to paint the check and dash.
void _initStrokePaint(Paint paint) {
paint
..color = checkColor
..style = PaintingStyle.stroke
..strokeWidth = _kStrokeWidth;
}
void _drawBorder(Canvas canvas, RRect outer, double t, Paint paint) {
assert(t >= 0.0 && t <= 0.5);
final double size = outer.width;
// As t goes from 0.0 to 1.0, gradually fill the outer RRect.
final RRect inner =
outer.deflate(math.min(size / 2.0, _kStrokeWidth + size * t));
canvas.drawDRRect(outer, inner, paint);
}
void _drawCheck(Canvas canvas, Offset origin, double t, Paint paint) {
assert(t >= 0.0 && t <= 1.0);
// As t goes from 0.0 to 1.0, animate the two check mark strokes from the
// short side to the long side.
final Path path = Path();
Offset start = Offset(_kEdgeSize * 0.15, _kEdgeSize * 0.45);
Offset mid = Offset(_kEdgeSize * 0.4, _kEdgeSize * 0.7);
Offset end = Offset(_kEdgeSize * 0.85, _kEdgeSize * 0.25);
if (t < 0.5) {
final double strokeT = t * 2.0;
final Offset drawMid = Offset.lerp(start, mid, strokeT);
path.moveTo(origin.dx + start.dx, origin.dy + start.dy);
path.lineTo(origin.dx + drawMid.dx, origin.dy + drawMid.dy);
} else {
final double strokeT = (t - 0.5) * 2.0;
final Offset drawEnd = Offset.lerp(mid, end, strokeT);
path.moveTo(origin.dx + start.dx, origin.dy + start.dy);
path.lineTo(origin.dx + mid.dx, origin.dy + mid.dy);
path.lineTo(origin.dx + drawEnd.dx, origin.dy + drawEnd.dy);
}
canvas.drawPath(path, paint);
}
void _drawDash(Canvas canvas, Offset origin, double t, Paint paint) {
assert(t >= 0.0 && t <= 1.0);
// As t goes from 0.0 to 1.0, animate the horizontal line from the
// mid point outwards.
Offset start = Offset(_kEdgeSize * 0.2, _kEdgeSize * 0.5);
Offset mid = Offset(_kEdgeSize * 0.5, _kEdgeSize * 0.5);
Offset end = Offset(_kEdgeSize * 0.8, _kEdgeSize * 0.5);
final Offset drawStart = Offset.lerp(start, mid, 1.0 - t);
final Offset drawEnd = Offset.lerp(mid, end, t);
canvas.drawLine(origin + drawStart, origin + drawEnd, paint);
}
@override
void paint(PaintingContext context, Offset offset) {
final Canvas canvas = context.canvas;
paintRadialReaction(canvas, offset, size.center(Offset.zero));
final Offset origin = offset + (size / 2.0 - Size.square(_kEdgeSize) / 2.0);
final AnimationStatus status = position.status;
final double tNormalized =
status == AnimationStatus.forward || status == AnimationStatus.completed
? position.value
: 1.0 - position.value;
// Four cases: false to null, false to true, null to false, true to false
if (_oldValue == false || value == false) {
final double t = value == false ? 1.0 - tNormalized : tNormalized;
final RRect outer = _outerRectAt(origin, t);
final Paint paint = Paint()..color = _colorAt(t);
if (t <= 0.5) {
_drawBorder(canvas, outer, t, paint);
} else {
canvas.drawRRect(outer, paint);
_initStrokePaint(paint);
final double tShrink = (t - 0.5) * 2.0;
if (_oldValue == null || value == null)
_drawDash(canvas, origin, tShrink, paint);
else
_drawCheck(canvas, origin, tShrink, paint);
}
} else {
// Two cases: null to true, true to null
final RRect outer = _outerRectAt(origin, 1.0);
final Paint paint = Paint()..color = _colorAt(1.0);
canvas.drawRRect(outer, paint);
_initStrokePaint(paint);
if (tNormalized <= 0.5) {
final double tShrink = 1.0 - tNormalized * 2.0;
if (_oldValue == true)
_drawCheck(canvas, origin, tShrink, paint);
else
_drawDash(canvas, origin, tShrink, paint);
} else {
final double tExpand = (tNormalized - 0.5) * 2.0;
if (value == true)
_drawCheck(canvas, origin, tExpand, paint);
else
_drawDash(canvas, origin, tExpand, paint);
}
}
}
}