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