blob: 5f65e0c8c0e0985675077ea59448a471b4cfc793 [file] [log] [blame]
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'constants.dart';
import 'debug.dart';
import 'theme.dart';
import 'toggleable.dart';
/// 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.
///
/// 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.
/// * <https://material.google.com/components/selection-controls.html#selection-controls-checkbox>
/// * <https://material.google.com/components/lists-controls.html#lists-controls-types-of-list-controls>
class Checkbox 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, and must not
/// be null.
/// * [onChanged], which is called when the value of the checkbox should
/// change. It can be set to null to disable the checkbox.
const Checkbox({
Key key,
@required this.value,
@required this.onChanged,
this.activeColor,
}) : assert(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 null, the checkbox will be displayed as disabled.
///
/// 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
/// new Checkbox(
/// value: _throwShotAway,
/// onChanged: (bool newValue) {
/// setState(() {
/// _throwShotAway = newValue;
/// });
/// },
/// )
/// ```
final ValueChanged<bool> onChanged;
/// The color to use when this checkbox is checked.
///
/// Defaults to accent color of the current [Theme].
final Color activeColor;
/// The width of a checkbox widget.
static const double width = 18.0;
@override
_CheckboxState createState() => new _CheckboxState();
}
class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin {
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context));
final ThemeData themeData = Theme.of(context);
return new _CheckboxRenderObjectWidget(
value: widget.value,
activeColor: widget.activeColor ?? themeData.accentColor,
inactiveColor: widget.onChanged != null ? themeData.unselectedWidgetColor : themeData.disabledColor,
onChanged: widget.onChanged,
vsync: this,
);
}
}
class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget {
const _CheckboxRenderObjectWidget({
Key key,
@required this.value,
@required this.activeColor,
@required this.inactiveColor,
@required this.onChanged,
@required this.vsync,
}) : assert(value != null),
assert(activeColor != null),
assert(inactiveColor != null),
assert(vsync != null),
super(key: key);
final bool value;
final Color activeColor;
final Color inactiveColor;
final ValueChanged<bool> onChanged;
final TickerProvider vsync;
@override
_RenderCheckbox createRenderObject(BuildContext context) => new _RenderCheckbox(
value: value,
activeColor: activeColor,
inactiveColor: inactiveColor,
onChanged: onChanged,
vsync: vsync,
);
@override
void updateRenderObject(BuildContext context, _RenderCheckbox renderObject) {
renderObject
..value = value
..activeColor = activeColor
..inactiveColor = inactiveColor
..onChanged = onChanged
..vsync = vsync;
}
}
const double _kEdgeSize = Checkbox.width;
const Radius _kEdgeRadius = const Radius.circular(1.0);
const double _kStrokeWidth = 2.0;
class _RenderCheckbox extends RenderToggleable {
_RenderCheckbox({
bool value,
Color activeColor,
Color inactiveColor,
ValueChanged<bool> onChanged,
@required TickerProvider vsync,
}): super(
value: value,
activeColor: activeColor,
inactiveColor: inactiveColor,
onChanged: onChanged,
size: const Size(2 * kRadialReactionRadius, 2 * kRadialReactionRadius),
vsync: vsync,
);
@override
void paint(PaintingContext context, Offset offset) {
final Canvas canvas = context.canvas;
final double offsetX = offset.dx + (size.width - _kEdgeSize) / 2.0;
final double offsetY = offset.dy + (size.height - _kEdgeSize) / 2.0;
paintRadialReaction(canvas, offset, size.center(Offset.zero));
final double t = position.value;
Color borderColor = inactiveColor;
if (onChanged != null)
borderColor = t >= 0.25 ? activeColor : Color.lerp(inactiveColor, activeColor, t * 4.0);
final Paint paint = new Paint()
..color = borderColor;
final double inset = 1.0 - (t - 0.5).abs() * 2.0;
final double rectSize = _kEdgeSize - inset * _kStrokeWidth;
final Rect rect = new Rect.fromLTWH(offsetX + inset, offsetY + inset, rectSize, rectSize);
final RRect outer = new RRect.fromRectAndRadius(rect, _kEdgeRadius);
if (t <= 0.5) {
// Outline
final RRect inner = outer.deflate(math.min(rectSize / 2.0, _kStrokeWidth + rectSize * t));
canvas.drawDRRect(outer, inner, paint);
} else {
// Background
canvas.drawRRect(outer, paint);
// White inner check
final double value = (t - 0.5) * 2.0;
paint
..color = const Color(0xFFFFFFFF)
..style = PaintingStyle.stroke
..strokeWidth = _kStrokeWidth;
final Path path = new Path();
final Offset start = const Offset(_kEdgeSize * 0.15, _kEdgeSize * 0.45);
final Offset mid = const Offset(_kEdgeSize * 0.4, _kEdgeSize * 0.7);
final Offset end = const Offset(_kEdgeSize * 0.85, _kEdgeSize * 0.25);
final Offset drawStart = Offset.lerp(start, mid, 1.0 - value);
final Offset drawEnd = Offset.lerp(mid, end, value);
path.moveTo(offsetX + drawStart.dx, offsetY + drawStart.dy);
path.lineTo(offsetX + mid.dx, offsetY + mid.dy);
path.lineTo(offsetX + drawEnd.dx, offsetY + drawEnd.dy);
canvas.drawPath(path, paint);
}
}
}