Commit 3f06da0e authored by Adam Barth's avatar Adam Barth

Improve Material selection controls

 - These controls now have proper radial reactions.
 - You can drag the switch.
 - The radio button animates properly.
 - There's a demo in the Material Gallery
parent 5873905e
// 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 'package:flutter/material.dart';
import 'widget_demo.dart';
class SelectionControlsDemo extends StatefulComponent {
_SelectionControlsDemoState createState() => new _SelectionControlsDemoState();
}
class _SelectionControlsDemoState extends State<SelectionControlsDemo> {
bool _checkboxValue = false;
int _radioValue = 0;
bool _switchValue = false;
void _setCheckboxValue(bool value) {
setState(() {
_checkboxValue = value;
});
}
void _setRadioValue(int value) {
setState(() {
_radioValue = value;
});
}
void _setSwitchValue(bool value) {
setState(() {
_switchValue = value;
});
}
Widget build(BuildContext context) {
return new Column([
new Row([
new Checkbox(value: _checkboxValue, onChanged: _setCheckboxValue),
new Checkbox(value: false), // Disabled
], justifyContent: FlexJustifyContent.spaceAround),
new Row([0, 1, 2].map((int i) {
return new Radio<int>(
value: i,
groupValue: _radioValue,
onChanged: _setRadioValue
);
}).toList(), justifyContent: FlexJustifyContent.spaceAround),
new Row([0, 1].map((int i) {
return new Radio<int>(value: i, groupValue: 0); // Disabled
}).toList(), justifyContent: FlexJustifyContent.spaceAround),
new Row([
new Switch(value: _switchValue, onChanged: _setSwitchValue),
new Switch(value: false), // Disabled
], justifyContent: FlexJustifyContent.spaceAround),
], justifyContent: FlexJustifyContent.spaceAround);
}
}
final WidgetDemo kSelectionControlsDemo = new WidgetDemo(
title: 'Selection Controls',
routeName: '/selection-controls',
builder: (_) => new SelectionControlsDemo()
);
......@@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
import 'demo/chip_demo.dart';
import 'demo/date_picker_demo.dart';
import 'demo/drop_down_demo.dart';
import 'demo/selection_controls_demo.dart';
import 'demo/slider_demo.dart';
import 'demo/time_picker_demo.dart';
import 'demo/widget_demo.dart';
......@@ -14,6 +15,7 @@ import 'gallery_page.dart';
final List<WidgetDemo> _kDemos = <WidgetDemo>[
kChipDemo,
kSelectionControlsDemo,
kSliderDemo,
kDatePickerDemo,
kTimePickerDemo,
......
// 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 'package:flutter/material.dart';
class BigSwitch extends StatefulComponent {
BigSwitch({ this.scale });
final double scale;
BigSwitchState createState() => new BigSwitchState();
}
class BigSwitchState extends State<BigSwitch> {
bool _value = false;
void _handleOnChanged(bool value) {
setState(() {
_value = value;
});
}
Widget build(BuildContext context) {
Matrix4 scale = new Matrix4.identity();
scale.scale(config.scale, config.scale);
return new Transform(
transform: scale,
child: new Switch(value: _value, onChanged: _handleOnChanged)
);
}
}
void main() {
runApp(new Container(
child: new BigSwitch(scale: 5.0),
padding: new EdgeDims.all(20.0),
decoration: new BoxDecoration(
backgroundColor: Colors.teal[600]
)
));
}
......@@ -8,14 +8,10 @@ import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'colors.dart';
import 'constants.dart';
import 'theme.dart';
import 'toggleable.dart';
const double _kMidpoint = 0.5;
const double _kEdgeSize = 18.0;
const double _kEdgeRadius = 1.0;
const double _kStrokeWidth = 2.0;
/// A material design checkbox
///
/// The checkbox itself does not maintain any state. Instead, when the state of
......@@ -39,15 +35,15 @@ class Checkbox extends StatelessComponent {
final bool value;
final ValueChanged<bool> onChanged;
bool get enabled => onChanged != null;
bool get _enabled => onChanged != null;
Widget build(BuildContext context) {
ThemeData themeData = Theme.of(context);
if (enabled) {
if (_enabled) {
Color uncheckedColor = themeData.brightness == ThemeBrightness.light
? Colors.black54
: Colors.white70;
return new _CheckboxWrapper(
return new _CheckboxRenderObjectWidget(
value: value,
onChanged: onChanged,
uncheckedColor: uncheckedColor,
......@@ -57,7 +53,7 @@ class Checkbox extends StatelessComponent {
Color disabledColor = themeData.brightness == ThemeBrightness.light
? Colors.black26
: Colors.white30;
return new _CheckboxWrapper(
return new _CheckboxRenderObjectWidget(
value: value,
uncheckedColor: disabledColor,
accentColor: disabledColor
......@@ -65,25 +61,22 @@ class Checkbox extends StatelessComponent {
}
}
// This wrapper class exists only because Switch needs to be a Component in
// order to get an accent color from a Theme but Components do not know how to
// host RenderObjects.
class _CheckboxWrapper extends LeafRenderObjectWidget {
_CheckboxWrapper({
class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget {
_CheckboxRenderObjectWidget({
Key key,
this.value,
this.onChanged,
this.uncheckedColor,
this.accentColor
this.accentColor,
this.onChanged
}) : super(key: key) {
assert(uncheckedColor != null);
assert(accentColor != null);
}
final bool value;
final ValueChanged<bool> onChanged;
final Color uncheckedColor;
final Color accentColor;
final ValueChanged<bool> onChanged;
_RenderCheckbox createRenderObject() => new _RenderCheckbox(
value: value,
......@@ -92,14 +85,20 @@ class _CheckboxWrapper extends LeafRenderObjectWidget {
onChanged: onChanged
);
void updateRenderObject(_RenderCheckbox renderObject, _CheckboxWrapper oldWidget) {
void updateRenderObject(_RenderCheckbox renderObject, _CheckboxRenderObjectWidget oldWidget) {
renderObject.value = value;
renderObject.onChanged = onChanged;
renderObject.uncheckedColor = uncheckedColor;
renderObject.accentColor = accentColor;
renderObject.onChanged = onChanged;
}
}
const double _kMidpoint = 0.5;
const double _kEdgeSize = 18.0;
const double _kEdgeRadius = 1.0;
const double _kStrokeWidth = 2.0;
const double _kOffset = kRadialReactionRadius - _kEdgeSize / 2.0;
class _RenderCheckbox extends RenderToggleable {
_RenderCheckbox({
bool value,
......@@ -107,19 +106,18 @@ class _RenderCheckbox extends RenderToggleable {
Color accentColor,
ValueChanged<bool> onChanged
}): _uncheckedColor = uncheckedColor,
_accentColor = accentColor,
super(
value: value,
accentColor: accentColor,
onChanged: onChanged,
size: new Size(_kEdgeSize, _kEdgeSize)
size: const Size(2 * kRadialReactionRadius, 2 * kRadialReactionRadius)
) {
assert(uncheckedColor != null);
assert(accentColor != null);
}
Color _uncheckedColor;
Color get uncheckedColor => _uncheckedColor;
Color _uncheckedColor;
void set uncheckedColor(Color value) {
assert(value != null);
if (value == _uncheckedColor)
......@@ -128,17 +126,13 @@ class _RenderCheckbox extends RenderToggleable {
markNeedsPaint();
}
Color _accentColor;
void set accentColor(Color value) {
assert(value != null);
if (value == _accentColor)
return;
_accentColor = value;
markNeedsPaint();
}
void paint(PaintingContext context, Offset offset) {
final PaintingCanvas canvas = context.canvas;
final Canvas canvas = context.canvas;
final double offsetX = _kOffset + offset.dx;
final double offsetY = _kOffset + offset.dy;
paintRadialReaction(canvas, offset + const Offset(kRadialReactionRadius, kRadialReactionRadius));
// Choose a color between grey and the theme color
Paint paint = new Paint()
..strokeWidth = _kStrokeWidth
......@@ -146,9 +140,9 @@ class _RenderCheckbox extends RenderToggleable {
// The rrect contracts slightly during the transition animation from checked states.
// Because we have a stroke size of 2, we should have a minimum 1.0 inset.
double inset = 2.0 - (position - _kMidpoint).abs() * 2.0;
double inset = 2.0 - (position.value - _kMidpoint).abs() * 2.0;
double rectSize = _kEdgeSize - inset * _kStrokeWidth;
Rect rect = new Rect.fromLTWH(offset.dx + inset, offset.dy + inset, rectSize, rectSize);
Rect rect = new Rect.fromLTWH(offsetX + inset, offsetY + inset, rectSize, rectSize);
// Create an inner rectangle to cover inside of rectangle. This is needed to avoid
// painting artefacts caused by overlayed paintings.
Rect innerRect = rect.deflate(1.0);
......@@ -160,24 +154,24 @@ class _RenderCheckbox extends RenderToggleable {
canvas.drawRRect(rrect, paint);
// Radial gradient that changes size
if (position > 0) {
if (!position.isDismissed) {
paint
..style = ui.PaintingStyle.fill
..shader = new ui.Gradient.radial(
new Point(_kEdgeSize / 2.0, _kEdgeSize / 2.0),
_kEdgeSize * (_kMidpoint - position) * 8.0, <Color>[
_kEdgeSize * (_kMidpoint - position.value) * 8.0, <Color>[
const Color(0x00000000),
uncheckedColor
]);
canvas.drawRect(innerRect, paint);
}
if (position > _kMidpoint) {
double t = (position - _kMidpoint) / (1.0 - _kMidpoint);
if (position.value > _kMidpoint) {
double t = (position.value - _kMidpoint) / (1.0 - _kMidpoint);
// First draw a rounded rect outline then fill inner rectangle with accent color.
paint
..color = new Color.fromARGB((t * 255).floor(), _accentColor.red, _accentColor.green, _accentColor.blue)
..color = accentColor.withAlpha((t * 255).floor())
..style = ui.PaintingStyle.stroke;
canvas.drawRRect(rrect, paint);
paint.style = ui.PaintingStyle.fill;
......@@ -195,9 +189,9 @@ class _RenderCheckbox extends RenderToggleable {
new Point(p1.x * (1.0 - t) + p2.x * t, p1.y * (1.0 - t) + p2.y * t);
Point drawStart = lerp(start, mid, 1.0 - t);
Point drawEnd = lerp(mid, end, t);
path.moveTo(offset.dx + drawStart.x, offset.dy + drawStart.y);
path.lineTo(offset.dx + mid.x, offset.dy + mid.y);
path.lineTo(offset.dx + drawEnd.x, offset.dy + drawEnd.y);
path.moveTo(offsetX + drawStart.x, offsetY + drawStart.y);
path.lineTo(offsetX + mid.x, offsetY + mid.y);
path.lineTo(offsetX + drawEnd.x, offsetY + drawEnd.y);
canvas.drawPath(path, paint);
}
}
......
......@@ -33,3 +33,7 @@ const double kPressedStateDuration = 64.0; // units?
const Duration kThemeChangeDuration = const Duration(milliseconds: 200);
const EdgeDims kDialogHeadingPadding = const EdgeDims.TRBL(24.0, 24.0, 20.0, 24.0);
const double kRadialReactionRadius = 24.0; // Pixels
const Duration kRadialReactionDuration = const Duration(milliseconds: 200);
const int kRadialReactionAlpha = 0x33;
......@@ -4,47 +4,18 @@
import 'dart:ui' as ui;
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'colors.dart';
import 'constants.dart';
import 'theme.dart';
import 'toggleable.dart';
const double _kDiameter = 16.0;
const double _kOuterRadius = _kDiameter / 2.0;
const double _kInnerRadius = 5.0;
class _RadioPainter extends CustomPainter {
const _RadioPainter({
this.color,
this.selected
});
final Color color;
final bool selected;
void paint(Canvas canvas, Size size) {
// TODO(ianh): ink radial reaction
// Draw the outer circle
Paint paint = new Paint()
..color = color
..style = ui.PaintingStyle.stroke
..strokeWidth = 2.0;
canvas.drawCircle(const Point(_kOuterRadius, _kOuterRadius), _kOuterRadius, paint);
// Draw the inner circle
if (selected) {
paint.style = ui.PaintingStyle.fill;
canvas.drawCircle(const Point(_kOuterRadius, _kOuterRadius), _kInnerRadius, paint);
}
}
bool shouldRepaint(_RadioPainter oldPainter) {
return oldPainter.color != color
|| oldPainter.selected != selected;
}
}
class Radio<T> extends StatelessComponent {
Radio({
Key key,
......@@ -57,31 +28,110 @@ class Radio<T> extends StatelessComponent {
final T groupValue;
final ValueChanged<T> onChanged;
bool get enabled => onChanged != null;
bool get _enabled => onChanged != null;
Color _getColor(BuildContext context) {
ThemeData themeData = Theme.of(context);
if (!enabled)
Color _getInactiveColor(ThemeData themeData) {
if (!_enabled)
return themeData.brightness == ThemeBrightness.light ? Colors.black26 : Colors.white30;
if (value == groupValue)
return themeData.accentColor;
return themeData.brightness == ThemeBrightness.light ? Colors.black54 : Colors.white70;
}
void _handleChanged(bool selected) {
if (selected)
onChanged(value);
}
Widget build(BuildContext context) {
return new GestureDetector(
onTap: enabled ? () => onChanged(value) : null,
child: new Container(
margin: const EdgeDims.symmetric(horizontal: 5.0),
width: _kDiameter,
height: _kDiameter,
child: new CustomPaint(
painter: new _RadioPainter(
color: _getColor(context),
selected: value == groupValue
)
)
)
ThemeData themeData = Theme.of(context);
return new _RadioRenderObjectWidget(
selected: value == groupValue,
inactiveColor: _getInactiveColor(themeData),
accentColor: themeData.accentColor,
onChanged: _enabled ? _handleChanged : null
);
}
}
class _RadioRenderObjectWidget extends LeafRenderObjectWidget {
_RadioRenderObjectWidget({
Key key,
this.selected,
this.inactiveColor,
this.accentColor,
this.onChanged
}) : super(key: key) {
assert(inactiveColor != null);
assert(accentColor != null);
}
final bool selected;
final Color inactiveColor;
final Color accentColor;
final ValueChanged<bool> onChanged;
_RenderRadio createRenderObject() => new _RenderRadio(
value: selected,
accentColor: accentColor,
inactiveColor: inactiveColor,
onChanged: onChanged
);
void updateRenderObject(_RenderRadio renderObject, _RadioRenderObjectWidget oldWidget) {
renderObject.value = selected;
renderObject.inactiveColor = inactiveColor;
renderObject.accentColor = accentColor;
renderObject.onChanged = onChanged;
}
}
class _RenderRadio extends RenderToggleable {
_RenderRadio({
bool value,
Color inactiveColor,
Color accentColor,
ValueChanged<bool> onChanged
}): _inactiveColor = inactiveColor,
super(
value: value,
accentColor: accentColor,
onChanged: onChanged,
size: const Size(2 * kRadialReactionRadius, 2 * kRadialReactionRadius)
) {
assert(inactiveColor != null);
assert(accentColor != null);
}
Color get inactiveColor => _inactiveColor;
Color _inactiveColor;
void set inactiveColor(Color value) {
assert(value != null);
if (value == _inactiveColor)
return;
_inactiveColor = value;
markNeedsPaint();
}
bool get isInteractive => super.isInteractive && !value;
void paint(PaintingContext context, Offset offset) {
final Canvas canvas = context.canvas;
paintRadialReaction(canvas, offset + const Offset(kRadialReactionRadius, kRadialReactionRadius));
Point center = (offset & size).center;
Color activeColor = onChanged != null ? accentColor : inactiveColor;
// Outer circle
Paint paint = new Paint()
..color = Color.lerp(inactiveColor, activeColor, position.value)
..style = ui.PaintingStyle.stroke
..strokeWidth = 2.0;
canvas.drawCircle(center, _kOuterRadius, paint);
// Inner circle
if (!position.isDismissed) {
paint.style = ui.PaintingStyle.fill;
canvas.drawCircle(center, _kInnerRadius * position.value, paint);
}
}
}
......@@ -9,14 +9,9 @@ import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'colors.dart';
import 'constants.dart';
import 'theme.dart';
const double _kThumbRadius = 6.0;
const double _kThumbRadiusDisabled = 3.0;
const double _kReactionRadius = 16.0;
const double _kTrackWidth = 144.0;
const int _kReactionAlpha = 0x33;
class Slider extends StatelessComponent {
Slider({ Key key, this.value, this.onChanged })
: super(key: key);
......@@ -54,9 +49,12 @@ class _SliderRenderObjectWidget extends LeafRenderObjectWidget {
}
}
const double _kThumbRadius = 6.0;
const double _kThumbRadiusDisabled = 3.0;
const double _kReactionRadius = 16.0;
const double _kTrackWidth = 144.0;
final Color _kInactiveTrackColor = Colors.grey[400];
final Color _kActiveTrackColor = Colors.grey[500];
const Duration _kRadialReactionDuration = const Duration(milliseconds: 200);
class _RenderSlider extends RenderConstrainedBox {
_RenderSlider({
......@@ -71,9 +69,9 @@ class _RenderSlider extends RenderConstrainedBox {
..onStart = _handleDragStart
..onUpdate = _handleDragUpdate
..onEnd = _handleDragEnd;
_reaction = new ValuePerformance(
_reaction = new ValuePerformance<double>(
variable: new AnimatedValue<double>(_kThumbRadius, end: _kReactionRadius, curve: Curves.ease),
duration: _kRadialReactionDuration
duration: kRadialReactionDuration
)..addListener(markNeedsPaint);
}
......@@ -162,7 +160,7 @@ class _RenderSlider extends RenderConstrainedBox {
Point activeLocation = new Point(trackActive, trackCenter);
if (_reaction.status != PerformanceStatus.dismissed) {
Paint reactionPaint = new Paint()..color = _primaryColor.withAlpha(_kReactionAlpha);
Paint reactionPaint = new Paint()..color = _primaryColor.withAlpha(kRadialReactionAlpha);
canvas.drawCircle(activeLocation, _reaction.value, reactionPaint);
}
canvas.drawCircle(activeLocation, thumbRadius, primaryPaint);
......
......@@ -2,30 +2,19 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:ui' as ui;
import 'package:flutter/animation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/painting.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'radial_reaction.dart';
import 'constants.dart';
import 'shadows.dart';
import 'theme.dart';
import 'toggleable.dart';
const Color _kThumbOffColor = const Color(0xFFFAFAFA);
const Color _kTrackOffColor = const Color(0x42000000);
const double _kSwitchWidth = 35.0;
const double _kThumbRadius = 10.0;
const double _kSwitchHeight = _kThumbRadius * 2.0;
const double _kTrackHeight = 14.0;
const double _kTrackRadius = _kTrackHeight / 2.0;
const double _kTrackWidth =
_kSwitchWidth - (_kThumbRadius - _kTrackRadius) * 2.0;
const Size _kSwitchSize = const Size(_kSwitchWidth + 2.0, _kSwitchHeight + 2.0);
const double _kReactionRadius = _kSwitchWidth / 2.0;
class Switch extends StatelessComponent {
Switch({ Key key, this.value, this.onChanged })
: super(key: key);
......@@ -34,117 +23,145 @@ class Switch extends StatelessComponent {
final ValueChanged<bool> onChanged;
Widget build(BuildContext context) {
return new _SwitchWrapper(
return new _SwitchRenderObjectWidget(
value: value,
thumbColor: Theme.of(context).accentColor,
accentColor: Theme.of(context).accentColor,
onChanged: onChanged
);
}
}
class _SwitchWrapper extends LeafRenderObjectWidget {
_SwitchWrapper({ Key key, this.value, this.thumbColor, this.onChanged })
: super(key: key);
class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
_SwitchRenderObjectWidget({
Key key,
this.value,
this.accentColor,
this.onChanged
}) : super(key: key);
final bool value;
final Color thumbColor;
final Color accentColor;
final ValueChanged<bool> onChanged;
_RenderSwitch createRenderObject() => new _RenderSwitch(
value: value,
thumbColor: thumbColor,
accentColor: accentColor,
onChanged: onChanged
);
void updateRenderObject(_RenderSwitch renderObject, _SwitchWrapper oldWidget) {
void updateRenderObject(_RenderSwitch renderObject, _SwitchRenderObjectWidget oldWidget) {
renderObject.value = value;
renderObject.thumbColor = thumbColor;
renderObject.accentColor = accentColor;
renderObject.onChanged = onChanged;
}
}
const Color _kThumbOffColor = const Color(0xFFFAFAFA);
const Color _kTrackOffColor = const Color(0x42000000);
const double _kTrackHeight = 14.0;
const double _kTrackWidth = 29.0;
const double _kTrackRadius = _kTrackHeight / 2.0;
const double _kThumbRadius = 10.0;
const double _kSwitchWidth = _kTrackWidth - 2 * _kTrackRadius + 2 * kRadialReactionRadius;
const double _kSwitchHeight = 2 * kRadialReactionRadius;
const int _kTrackAlpha = 0x80;
class _RenderSwitch extends RenderToggleable {
_RenderSwitch({
bool value,
Color thumbColor: _kThumbOffColor,
Color accentColor,
ValueChanged<bool> onChanged
}) : _thumbColor = thumbColor,
super(value: value, onChanged: onChanged, size: _kSwitchSize);
Color _thumbColor;
Color get thumbColor => _thumbColor;
void set thumbColor(Color value) {
if (value == _thumbColor) return;
_thumbColor = value;
markNeedsPaint();
}) : super(
value: value,
accentColor: accentColor,
onChanged: onChanged,
minRadialReactionRadius: _kThumbRadius,
size: const Size(_kSwitchWidth, _kSwitchHeight)
) {
_drag = new HorizontalDragGestureRecognizer(router: FlutterBinding.instance.pointerRouter)
..onStart = _handleDragStart
..onUpdate = _handleDragUpdate
..onEnd = _handleDragEnd;
}
RadialReaction _radialReaction;
double get _trackInnerLength => size.width - 2.0 * kRadialReactionRadius;
void handleEvent(InputEvent event, BoxHitTestEntry entry) {
if (event is PointerInputEvent) {
if (event.type == 'pointerdown')
_showRadialReaction(entry.localPosition);
else if (event.type == 'pointerup')
_hideRadialReaction();
HorizontalDragGestureRecognizer _drag;
void _handleDragStart(Point globalPosition) {
if (onChanged != null)
reaction.forward();
}
void _handleDragUpdate(double delta) {
if (onChanged != null) {
position.variable
..curve = null
..reverseCurve = null;
position.progress += delta / _trackInnerLength;
}
super.handleEvent(event, entry);
}
void _showRadialReaction(Point startLocation) {
if (_radialReaction != null)
return;
_radialReaction = new RadialReaction(
center: new Point(_kSwitchSize.width / 2.0, _kSwitchSize.height / 2.0),
radius: _kReactionRadius,
startPosition: startLocation
)..addListener(markNeedsPaint)
..show();
void _handleDragEnd(Offset velocity) {
if (position.progress >= 0.5)
position.forward();
else
position.reverse();
reaction.reverse();
}
Future _hideRadialReaction() async {
if (_radialReaction == null)
return;
await _radialReaction.hide();
_radialReaction = null;
void handleEvent(InputEvent event, BoxHitTestEntry entry) {
if (event.type == 'pointerdown' && onChanged != null)
_drag.addPointer(event);
super.handleEvent(event, entry);
}
final BoxPainter _thumbPainter = new BoxPainter(const BoxDecoration());
void paint(PaintingContext context, Offset offset) {
final PaintingCanvas canvas = context.canvas;
Color thumbColor = _kThumbOffColor;
Color trackColor = _kTrackOffColor;
if (value) {
thumbColor = _thumbColor;
trackColor = new Color(_thumbColor.value & 0x80FFFFFF);
if (position.status == PerformanceStatus.forward
|| position.status == PerformanceStatus.completed) {
thumbColor = accentColor;
trackColor = accentColor.withAlpha(_kTrackAlpha);
}
// Draw the track rrect
// Paint the track
Paint paint = new Paint()
..color = trackColor
..style = ui.PaintingStyle.fill;
Rect rect = new Rect.fromLTWH(offset.dx,
offset.dy + _kSwitchHeight / 2.0 - _kTrackHeight / 2.0, _kTrackWidth,
_kTrackHeight);
ui.RRect rrect = new ui.RRect.fromRectXY(
rect, _kTrackRadius, _kTrackRadius);
canvas.drawRRect(rrect, paint);
if (_radialReaction != null)
_radialReaction.paint(canvas, offset);
// Draw the raised thumb with a shadow
paint.color = thumbColor;
ShadowDrawLooperBuilder builder = new ShadowDrawLooperBuilder();
for (BoxShadow boxShadow in elevationToShadow[1])
builder.addShadow(boxShadow.offset, boxShadow.color, boxShadow.blurRadius);
paint.drawLooper = builder.build();
..color = trackColor;
double trackHorizontalPadding = kRadialReactionRadius - _kTrackRadius;
Rect trackRect = new Rect.fromLTWH(
offset.dx + trackHorizontalPadding,
offset.dy + (size.height - _kTrackHeight) / 2.0,
size.width - 2.0 * trackHorizontalPadding,
_kTrackHeight
);
ui.RRect trackRRect = new ui.RRect.fromRectXY(
trackRect, _kTrackRadius, _kTrackRadius);
canvas.drawRRect(trackRRect, paint);
Offset thumbOffset = new Offset(
offset.dx + kRadialReactionRadius + position.value * _trackInnerLength,
offset.dy + size.height / 2.0);
paintRadialReaction(canvas, thumbOffset);
_thumbPainter.decoration = new BoxDecoration(
backgroundColor: thumbColor,
shape: Shape.circle,
boxShadow: elevationToShadow[1]
);
// The thumb contracts slightly during the animation
double inset = 2.0 - (position - 0.5).abs() * 2.0;
Point thumbPos = new Point(offset.dx +
_kTrackRadius +
position * (_kTrackWidth - _kTrackRadius * 2),
offset.dy + _kSwitchHeight / 2.0);
canvas.drawCircle(thumbPos, _kThumbRadius - inset, paint);
double inset = 2.0 - (position.value - 0.5).abs() * 2.0;
double radius = _kThumbRadius - inset;
Rect thumbRect = new Rect.fromLTRB(thumbOffset.dx - radius,
thumbOffset.dy - radius,
thumbOffset.dx + radius,
thumbOffset.dy + radius);
_thumbPainter.paint(canvas, thumbRect);
}
}
......@@ -6,6 +6,8 @@ import 'package:flutter/animation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'constants.dart';
const Duration _kToggleDuration = const Duration(milliseconds: 200);
// RenderToggleable is a base class for material style toggleable controls with
......@@ -16,13 +18,26 @@ abstract class RenderToggleable extends RenderConstrainedBox {
RenderToggleable({
bool value,
Size size,
this.onChanged
Color accentColor,
this.onChanged,
double minRadialReactionRadius: 0.0
}) : _value = value,
_accentColor = accentColor,
super(additionalConstraints: new BoxConstraints.tight(size)) {
_performance = new ValuePerformance<double>(
variable: new AnimatedValue<double>(0.0, end: 1.0, curve: Curves.easeIn, reverseCurve: Curves.easeOut),
_tap = new TapGestureRecognizer(router: FlutterBinding.instance.pointerRouter)
..onTapDown = _handleTapDown
..onTap = _handleTap
..onTapUp = _handleTapUp
..onTapCancel = _handleTapCancel;
_position = new ValuePerformance<double>(
variable: new AnimatedValue<double>(0.0, end: 1.0),
duration: _kToggleDuration,
progress: _value ? 1.0 : 0.0
)..addListener(markNeedsPaint)
..addStatusListener(_handlePositionStateChanged);
_reaction = new ValuePerformance<double>(
variable: new AnimatedValue<double>(minRadialReactionRadius, end: kRadialReactionRadius, curve: Curves.ease),
duration: kRadialReactionDuration
)..addListener(markNeedsPaint);
}
......@@ -32,41 +47,73 @@ abstract class RenderToggleable extends RenderConstrainedBox {
if (value == _value)
return;
_value = value;
performance.play(value ? AnimationDirection.forward : AnimationDirection.reverse);
_position.variable
..curve = Curves.easeIn
..reverseCurve = Curves.easeOut;
_position.play(value ? AnimationDirection.forward : AnimationDirection.reverse);
}
Color get accentColor => _accentColor;
Color _accentColor;
void set accentColor(Color value) {
if (value == _accentColor)
return;
_accentColor = value;
markNeedsPaint();
}
bool get isInteractive => onChanged != null;
ValueChanged<bool> onChanged;
ValuePerformance<double> get performance => _performance;
ValuePerformance<double> _performance;
ValuePerformance<double> get position => _position;
ValuePerformance<double> _position;
double get position => _performance.value;
ValuePerformance<double> get reaction => _reaction;
ValuePerformance<double> _reaction;
TapGestureRecognizer _tap;
void attach() {
super.attach();
_tap = new TapGestureRecognizer(
router: FlutterBinding.instance.pointerRouter,
onTap: _handleTap
);
void _handlePositionStateChanged(PerformanceStatus status) {
if (isInteractive) {
if (status == PerformanceStatus.completed && !_value)
onChanged(true);
else if (status == PerformanceStatus.dismissed && _value)
onChanged(false);
}
void detach() {
_tap.dispose();
_tap = null;
super.detach();
}
void handleEvent(InputEvent event, BoxHitTestEntry entry) {
if (event.type == 'pointerdown' && onChanged != null)
_tap.addPointer(event);
void _handleTapDown(Point globalPosition) {
if (isInteractive)
_reaction.forward();
}
void _handleTap() {
if (onChanged != null)
if (isInteractive)
onChanged(!_value);
}
void _handleTapUp(Point globalPosition) {
if (isInteractive)
_reaction.reverse();
}
void _handleTapCancel() {
if (isInteractive)
_reaction.reverse();
}
bool hitTestSelf(Point position) => true;
void handleEvent(InputEvent event, BoxHitTestEntry entry) {
if (event.type == 'pointerdown' && isInteractive)
_tap.addPointer(event);
}
void paintRadialReaction(Canvas canvas, Offset offset) {
if (!reaction.isDismissed) {
Paint reactionPaint = new Paint()..color = accentColor.withAlpha(kRadialReactionAlpha);
canvas.drawCircle(offset.toPoint(), reaction.value, reactionPaint);
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment