mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Add RenderToggleable base and use in Switch and Checkbox
This refactors Checkbox to own a RenderObject similar to how Switch was refactored in https://github.com/domokit/sky_engine/pull/376 and extracts common functionality for toggleable renderers into a base class. Switch and Checkbox's render objects derive from this base class to add their own custom painting and theming logic.
This commit is contained in:
parent
fed00af7a2
commit
9593b80d69
@ -104,7 +104,7 @@ class StockSettings extends StatefulComponent {
|
||||
onPressed: () => _confirmOptimismChange(),
|
||||
children: [
|
||||
new Flexible(child: new Text('Everything is awesome')),
|
||||
new Checkbox(value: optimism == StockMode.optimistic, onChanged: _handleOptimismChanged)
|
||||
new Checkbox(value: optimism == StockMode.optimistic, onChanged: (_) => _confirmOptimismChange())
|
||||
]
|
||||
),
|
||||
new DrawerItem(
|
||||
|
||||
71
sky/packages/sky/lib/rendering/toggleable.dart
Normal file
71
sky/packages/sky/lib/rendering/toggleable.dart
Normal file
@ -0,0 +1,71 @@
|
||||
// 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:sky' as sky;
|
||||
|
||||
import 'package:sky/animation/animated_value.dart';
|
||||
import 'package:sky/animation/animation_performance.dart';
|
||||
import 'package:sky/animation/curves.dart';
|
||||
import 'package:sky/rendering/box.dart';
|
||||
import 'package:sky/rendering/object.dart';
|
||||
|
||||
typedef void ValueChanged(bool value);
|
||||
|
||||
const Duration _kToggleDuration = const Duration(milliseconds: 200);
|
||||
|
||||
// RenderToggleable is a base class for material style toggleable controls with
|
||||
// toggle animations. It handles storing the current value, dispatching
|
||||
// ValueChanged on a tap gesture and driving a changed animation. Subclasses are
|
||||
// responsible for painting.
|
||||
abstract class RenderToggleable extends RenderConstrainedBox {
|
||||
RenderToggleable({bool value, Size size, ValueChanged onChanged})
|
||||
: _value = value,
|
||||
_onChanged = onChanged,
|
||||
super(additionalConstraints: new BoxConstraints.tight(size)) {
|
||||
_performance = new AnimationPerformance()
|
||||
..variable = _position
|
||||
..duration = _kToggleDuration
|
||||
..progress = _value ? 1.0 : 0.0
|
||||
..addListener(markNeedsPaint);
|
||||
}
|
||||
|
||||
EventDisposition handleEvent(sky.Event event, BoxHitTestEntry entry) {
|
||||
if (event is sky.GestureEvent && event.type == 'gesturetap') {
|
||||
_onChanged(!_value);
|
||||
return EventDisposition.consumed;
|
||||
}
|
||||
return EventDisposition.ignored;
|
||||
}
|
||||
|
||||
bool _value;
|
||||
bool get value => _value;
|
||||
|
||||
void set value(bool value) {
|
||||
if (value == _value) return;
|
||||
_value = value;
|
||||
// TODO(abarth): Setting the curve on the position means there's a
|
||||
// discontinuity when we reverse the timeline.
|
||||
if (value) {
|
||||
_position.curve = easeIn;
|
||||
_performance.play();
|
||||
} else {
|
||||
_position.curve = easeOut;
|
||||
_performance.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
ValueChanged _onChanged;
|
||||
ValueChanged get onChanged => _onChanged;
|
||||
|
||||
void set onChanged(ValueChanged onChanged) {
|
||||
_onChanged = onChanged;
|
||||
}
|
||||
|
||||
final AnimatedValue<double> _position =
|
||||
new AnimatedValue<double>(0.0, end: 1.0);
|
||||
AnimatedValue<double> get position => _position;
|
||||
|
||||
AnimationPerformance _performance;
|
||||
AnimationPerformance get performance => _performance;
|
||||
}
|
||||
@ -37,7 +37,6 @@ export 'widgets/switch.dart';
|
||||
export 'widgets/tabs.dart';
|
||||
export 'widgets/task_description.dart';
|
||||
export 'widgets/theme.dart';
|
||||
export 'widgets/toggleable.dart';
|
||||
export 'widgets/tool_bar.dart';
|
||||
export 'widgets/transitions.dart';
|
||||
export 'widgets/widget.dart';
|
||||
|
||||
@ -23,10 +23,9 @@ export 'package:sky/base/hit_test.dart' show EventDisposition;
|
||||
export 'package:sky/rendering/box.dart' show BackgroundImage, BoxConstraints, BoxDecoration, Border, BorderSide, EdgeDims;
|
||||
export 'package:sky/rendering/flex.dart' show FlexDirection, FlexJustifyContent, FlexAlignItems;
|
||||
export 'package:sky/rendering/object.dart' show Point, Offset, Size, Rect, Color, Paint, Path;
|
||||
export 'package:sky/rendering/toggleable.dart' show ValueChanged;
|
||||
export 'package:sky/widgets/widget.dart' show Key, GlobalKey, Widget, Component, StatefulComponent, App, runApp, Listener, ParentDataNode;
|
||||
|
||||
typedef void ValueChanged(bool);
|
||||
|
||||
// PAINTING NODES
|
||||
|
||||
class Opacity extends OneChildRenderObjectWrapper {
|
||||
|
||||
@ -4,17 +4,20 @@
|
||||
|
||||
import 'dart:sky' as sky;
|
||||
|
||||
import 'package:sky/widgets/theme.dart';
|
||||
import 'package:sky/rendering/object.dart';
|
||||
import 'package:sky/widgets/basic.dart';
|
||||
import 'package:sky/widgets/toggleable.dart';
|
||||
import 'package:sky/widgets/theme.dart';
|
||||
import 'package:sky/widgets/widget.dart';
|
||||
import 'package:sky/rendering/toggleable.dart';
|
||||
|
||||
export 'toggleable.dart' show ValueChanged;
|
||||
export 'package:sky/rendering/toggleable.dart' show ValueChanged;
|
||||
|
||||
const double _kMidpoint = 0.5;
|
||||
const sky.Color _kLightUncheckedColor = const sky.Color(0x8A000000);
|
||||
const sky.Color _kDarkUncheckedColor = const sky.Color(0xB2FFFFFF);
|
||||
const double _kEdgeSize = 20.0;
|
||||
const double _kEdgeRadius = 1.0;
|
||||
const Duration _kCheckDuration = const Duration(milliseconds: 200);
|
||||
|
||||
/// A material design checkbox
|
||||
///
|
||||
@ -25,32 +28,91 @@ const double _kEdgeRadius = 1.0;
|
||||
/// the checkbox.
|
||||
///
|
||||
/// <https://www.google.com/design/spec/components/lists-controls.html#lists-controls-types-of-list-controls>
|
||||
class Checkbox extends Toggleable {
|
||||
|
||||
class Checkbox extends Component {
|
||||
/// Constructs a checkbox
|
||||
///
|
||||
/// * `value` determines whether the checkbox is checked.
|
||||
/// * `onChanged` is called whenever the state of the checkbox should change.
|
||||
Checkbox({
|
||||
Key key,
|
||||
bool value,
|
||||
ValueChanged onChanged
|
||||
}) : super(key: key, value: value, onChanged: onChanged);
|
||||
Checkbox({Key key, this.value, this.onChanged}) : super(key: key);
|
||||
|
||||
Size get size => const Size(_kEdgeSize + 2.0, _kEdgeSize + 2.0);
|
||||
final bool value;
|
||||
final ValueChanged onChanged;
|
||||
|
||||
void customPaintCallback(sky.Canvas canvas, Size size) {
|
||||
Widget build() {
|
||||
ThemeData themeData = Theme.of(this);
|
||||
Color uncheckedColor = themeData.brightness == ThemeBrightness.light ? _kLightUncheckedColor : _kDarkUncheckedColor;
|
||||
Color uncheckedColor = themeData.brightness == ThemeBrightness.light
|
||||
? _kLightUncheckedColor
|
||||
: _kDarkUncheckedColor;
|
||||
return new _CheckboxWrapper(
|
||||
value: value,
|
||||
onChanged: onChanged,
|
||||
uncheckedColor: uncheckedColor,
|
||||
accentColor: themeData.accentColor);
|
||||
}
|
||||
}
|
||||
|
||||
// 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 LeafRenderObjectWrapper {
|
||||
_CheckboxWrapper({Key key, this.value, this.onChanged, this.uncheckedColor,
|
||||
this.accentColor})
|
||||
: super(key: key);
|
||||
|
||||
final bool value;
|
||||
final ValueChanged onChanged;
|
||||
final Color uncheckedColor;
|
||||
final Color accentColor;
|
||||
|
||||
_RenderCheckbox get root => super.root;
|
||||
_RenderCheckbox createNode() => new _RenderCheckbox(
|
||||
value: value, uncheckedColor: uncheckedColor, onChanged: onChanged);
|
||||
|
||||
void syncRenderObject(_CheckboxWrapper old) {
|
||||
super.syncRenderObject(old);
|
||||
root.value = value;
|
||||
root.onChanged = onChanged;
|
||||
root.uncheckedColor = uncheckedColor;
|
||||
root.accentColor = accentColor;
|
||||
}
|
||||
}
|
||||
|
||||
class _RenderCheckbox extends RenderToggleable {
|
||||
_RenderCheckbox({bool value, Color uncheckedColor, ValueChanged onChanged})
|
||||
: _uncheckedColor = uncheckedColor,
|
||||
super(
|
||||
value: value,
|
||||
onChanged: onChanged,
|
||||
size: new Size(_kEdgeSize, _kEdgeSize)) {}
|
||||
|
||||
Color _uncheckedColor;
|
||||
Color get uncheckedColor => _uncheckedColor;
|
||||
|
||||
void set uncheckedColor(Color value) {
|
||||
if (value == _uncheckedColor) return;
|
||||
_uncheckedColor = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
Color _accentColor;
|
||||
void set accentColor(Color value) {
|
||||
if (value == _accentColor) return;
|
||||
_accentColor = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
void paint(PaintingCanvas canvas, Offset offset) {
|
||||
// Choose a color between grey and the theme color
|
||||
sky.Paint paint = new sky.Paint()..strokeWidth = 2.0
|
||||
..color = uncheckedColor;
|
||||
sky.Paint paint = new sky.Paint()
|
||||
..strokeWidth = 2.0
|
||||
..color = uncheckedColor;
|
||||
|
||||
// The rrect contracts slightly during the animation
|
||||
double inset = 2.0 - (position.value - _kMidpoint).abs() * 2.0;
|
||||
sky.Rect rect = new sky.Rect.fromLTRB(inset, inset, _kEdgeSize - inset, _kEdgeSize - inset);
|
||||
sky.RRect rrect = new sky.RRect()..setRectXY(rect, _kEdgeRadius, _kEdgeRadius);
|
||||
sky.Rect rect = new sky.Rect.fromLTWH(offset.dx + inset, offset.dy + inset,
|
||||
_kEdgeSize - inset, _kEdgeSize - inset);
|
||||
sky.RRect rrect = new sky.RRect()
|
||||
..setRectXY(rect, _kEdgeRadius, _kEdgeRadius);
|
||||
|
||||
// Outline of the empty rrect
|
||||
paint.setStyle(sky.PaintingStyle.stroke);
|
||||
@ -59,13 +121,12 @@ class Checkbox extends Toggleable {
|
||||
// Radial gradient that changes size
|
||||
if (position.value > 0) {
|
||||
paint.setStyle(sky.PaintingStyle.fill);
|
||||
paint.setShader(
|
||||
new sky.Gradient.radial(
|
||||
paint.setShader(new sky.Gradient.radial(
|
||||
new Point(_kEdgeSize / 2.0, _kEdgeSize / 2.0),
|
||||
_kEdgeSize * (_kMidpoint - position.value) * 8.0,
|
||||
[const sky.Color(0x00000000), uncheckedColor]
|
||||
)
|
||||
);
|
||||
_kEdgeSize * (_kMidpoint - position.value) * 8.0, [
|
||||
const sky.Color(0x00000000),
|
||||
uncheckedColor
|
||||
]));
|
||||
canvas.drawRRect(rrect, paint);
|
||||
}
|
||||
|
||||
@ -74,10 +135,8 @@ class Checkbox extends Toggleable {
|
||||
|
||||
// Solid filled rrect
|
||||
paint.setStyle(sky.PaintingStyle.strokeAndFill);
|
||||
paint.color = new Color.fromARGB((t * 255).floor(),
|
||||
themeData.accentColor.red,
|
||||
themeData.accentColor.green,
|
||||
themeData.accentColor.blue);
|
||||
paint.color = new Color.fromARGB((t * 255).floor(), _accentColor.red,
|
||||
_accentColor.green, _accentColor.blue);
|
||||
canvas.drawRRect(rrect, paint);
|
||||
|
||||
// White inner check
|
||||
@ -87,13 +146,13 @@ class Checkbox extends Toggleable {
|
||||
sky.Point start = new sky.Point(_kEdgeSize * 0.2, _kEdgeSize * 0.5);
|
||||
sky.Point mid = new sky.Point(_kEdgeSize * 0.4, _kEdgeSize * 0.7);
|
||||
sky.Point end = new sky.Point(_kEdgeSize * 0.8, _kEdgeSize * 0.3);
|
||||
Point lerp(Point p1, Point p2, double t)
|
||||
=> new Point(p1.x * (1.0 - t) + p2.x * t, p1.y * (1.0 - t) + p2.y * t);
|
||||
Point lerp(Point p1, Point p2, double t) =>
|
||||
new Point(p1.x * (1.0 - t) + p2.x * t, p1.y * (1.0 - t) + p2.y * t);
|
||||
sky.Point drawStart = lerp(start, mid, 1.0 - t);
|
||||
sky.Point drawEnd = lerp(mid, end, t);
|
||||
path.moveTo(drawStart.x, drawStart.y);
|
||||
path.lineTo(mid.x, mid.y);
|
||||
path.lineTo(drawEnd.x, drawEnd.y);
|
||||
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);
|
||||
canvas.drawPath(path, paint);
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,9 +4,6 @@
|
||||
|
||||
import 'dart:sky' as sky;
|
||||
|
||||
import 'package:sky/animation/animated_value.dart';
|
||||
import 'package:sky/animation/animation_performance.dart';
|
||||
import 'package:sky/animation/curves.dart';
|
||||
import 'package:sky/painting/shadows.dart';
|
||||
import 'package:sky/rendering/box.dart';
|
||||
import 'package:sky/rendering/object.dart';
|
||||
@ -14,8 +11,9 @@ import 'package:sky/theme/shadows.dart';
|
||||
import 'package:sky/widgets/basic.dart';
|
||||
import 'package:sky/widgets/theme.dart';
|
||||
import 'package:sky/widgets/widget.dart';
|
||||
import 'package:sky/rendering/toggleable.dart';
|
||||
|
||||
export 'package:sky/widgets/basic.dart' show ValueChanged;
|
||||
export 'package:sky/rendering/toggleable.dart' show ValueChanged;
|
||||
|
||||
const sky.Color _kThumbOffColor = const sky.Color(0xFFFAFAFA);
|
||||
const sky.Color _kTrackOffColor = const sky.Color(0x42000000);
|
||||
@ -66,47 +64,14 @@ class _SwitchWrapper extends LeafRenderObjectWrapper {
|
||||
}
|
||||
}
|
||||
|
||||
class _RenderSwitch extends RenderConstrainedBox {
|
||||
class _RenderSwitch extends RenderToggleable {
|
||||
_RenderSwitch(
|
||||
{bool value, Color thumbColor: _kThumbOffColor, ValueChanged onChanged})
|
||||
: _value = value,
|
||||
_thumbColor = thumbColor,
|
||||
_onChanged = onChanged,
|
||||
super(additionalConstraints: new BoxConstraints.tight(_kSwitchSize)) {
|
||||
_performance = new AnimationPerformance()
|
||||
..variable = _position
|
||||
..duration = _kCheckDuration
|
||||
..progress = _value ? 1.0 : 0.0
|
||||
..addListener(markNeedsPaint);
|
||||
}
|
||||
|
||||
EventDisposition handleEvent(sky.Event event, BoxHitTestEntry entry) {
|
||||
if (event is sky.GestureEvent && event.type == 'gesturetap') {
|
||||
_onChanged(!_value);
|
||||
return EventDisposition.consumed;
|
||||
}
|
||||
return EventDisposition.ignored;
|
||||
}
|
||||
|
||||
bool _value;
|
||||
bool get value => _value;
|
||||
|
||||
void set value(bool value) {
|
||||
if (value == _value) return;
|
||||
_value = value;
|
||||
// TODO(abarth): Setting the curve on the position means there's a
|
||||
// discontinuity when we reverse the timeline.
|
||||
if (value) {
|
||||
_position.curve = easeIn;
|
||||
_performance.play();
|
||||
} else {
|
||||
_position.curve = easeOut;
|
||||
_performance.reverse();
|
||||
}
|
||||
}
|
||||
: _thumbColor = thumbColor,
|
||||
super(value: value, onChanged: onChanged, size: _kSwitchSize) {}
|
||||
|
||||
Color _thumbColor;
|
||||
Color get thumbColor => _thumbColor;
|
||||
Color get thumbColor => _thumbColor;
|
||||
|
||||
void set thumbColor(Color value) {
|
||||
if (value == _thumbColor) return;
|
||||
@ -114,22 +79,10 @@ class _RenderSwitch extends RenderConstrainedBox {
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
ValueChanged _onChanged;
|
||||
ValueChanged get onChanged => _onChanged;
|
||||
|
||||
void set onChanged(ValueChanged onChanged) {
|
||||
_onChanged = onChanged;
|
||||
}
|
||||
|
||||
final AnimatedValue<double> _position =
|
||||
new AnimatedValue<double>(0.0, end: 1.0);
|
||||
|
||||
AnimationPerformance _performance;
|
||||
|
||||
void paint(PaintingCanvas canvas, Offset offset) {
|
||||
sky.Color thumbColor = _kThumbOffColor;
|
||||
sky.Color trackColor = _kTrackOffColor;
|
||||
if (_value) {
|
||||
if (value) {
|
||||
thumbColor = _thumbColor;
|
||||
trackColor = new sky.Color(_thumbColor.value & 0x80FFFFFF);
|
||||
}
|
||||
@ -152,10 +105,10 @@ class _RenderSwitch extends RenderConstrainedBox {
|
||||
paint.setDrawLooper(builder.build());
|
||||
|
||||
// The thumb contracts slightly during the animation
|
||||
double inset = 2.0 - (_position.value - 0.5).abs() * 2.0;
|
||||
double inset = 2.0 - (position.value - 0.5).abs() * 2.0;
|
||||
Point thumbPos = new Point(offset.dx +
|
||||
_kTrackRadius +
|
||||
_position.value * (_kTrackWidth - _kTrackRadius * 2),
|
||||
position.value * (_kTrackWidth - _kTrackRadius * 2),
|
||||
offset.dy + _kSwitchHeight / 2.0);
|
||||
canvas.drawCircle(thumbPos, _kThumbRadius - inset, paint);
|
||||
}
|
||||
|
||||
@ -1,88 +0,0 @@
|
||||
// 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:sky' as sky;
|
||||
|
||||
import 'package:sky/animation/animated_value.dart';
|
||||
import 'package:sky/animation/animation_performance.dart';
|
||||
import 'package:sky/animation/curves.dart';
|
||||
import 'package:sky/widgets/animated_component.dart';
|
||||
import 'package:sky/widgets/basic.dart';
|
||||
|
||||
export 'package:sky/widgets/basic.dart' show ValueChanged;
|
||||
|
||||
const Duration _kCheckDuration = const Duration(milliseconds: 200);
|
||||
|
||||
abstract class Toggleable extends AnimatedComponent {
|
||||
|
||||
Toggleable({
|
||||
Key key,
|
||||
this.value,
|
||||
this.onChanged
|
||||
}) : super(key: key);
|
||||
|
||||
bool value;
|
||||
ValueChanged onChanged;
|
||||
|
||||
AnimatedValue<double> _position;
|
||||
AnimatedValue<double> get position => _position;
|
||||
|
||||
AnimationPerformance _performance;
|
||||
AnimationPerformance get performance => _performance;
|
||||
|
||||
void initState() {
|
||||
_position = new AnimatedValue<double>(0.0, end: 1.0);
|
||||
_performance = new AnimationPerformance()
|
||||
..variable = position
|
||||
..duration = _kCheckDuration
|
||||
..progress = value ? 1.0 : 0.0;
|
||||
watch(performance);
|
||||
}
|
||||
|
||||
void syncFields(Toggleable source) {
|
||||
onChanged = source.onChanged;
|
||||
if (value != source.value) {
|
||||
value = source.value;
|
||||
// TODO(abarth): Setting the curve on the position means there's a
|
||||
// discontinuity when we reverse the timeline.
|
||||
if (value) {
|
||||
position.curve = curveUp;
|
||||
performance.play();
|
||||
} else {
|
||||
position.curve = curveDown;
|
||||
performance.reverse();
|
||||
}
|
||||
}
|
||||
super.syncFields(source);
|
||||
}
|
||||
|
||||
EventDisposition _handleClick(sky.Event e) {
|
||||
onChanged(!value);
|
||||
return EventDisposition.consumed;
|
||||
}
|
||||
|
||||
// Override these to draw yourself
|
||||
void customPaintCallback(sky.Canvas canvas, Size size);
|
||||
Size get size;
|
||||
|
||||
EdgeDims get margin => const EdgeDims.symmetric(horizontal: 5.0);
|
||||
double get duration => 200.0;
|
||||
Curve get curveUp => easeIn;
|
||||
Curve get curveDown => easeOut;
|
||||
|
||||
Widget build() {
|
||||
return new Listener(
|
||||
child: new Container(
|
||||
margin: margin,
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
child: new CustomPaint(
|
||||
token: position.value,
|
||||
callback: customPaintCallback
|
||||
)
|
||||
),
|
||||
onGestureTap: _handleClick
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user