From 445f4ee8cd456a1ea72b383fcc90d5fad97e826d Mon Sep 17 00:00:00 2001 From: James Robinson Date: Thu, 30 Jul 2015 10:39:08 -0700 Subject: [PATCH] Make Switch a standalone class with a custom RenderObject This changes Switch from being a subclass of the Toggleable widget to being a standalone Component with a custom RenderObject. This is to enable adding Switch-specific radial reaction animations in a subsequent patch. The CustomPaint logic that Toggleable was using does not provide a nice way for a class and its subclass to both participate in deciding to repaint. --- .../flutter/example/widgets/big_switch.dart | 35 +++++ packages/flutter/lib/widgets/basic.dart | 1 + packages/flutter/lib/widgets/switch.dart | 146 ++++++++++++++---- packages/flutter/lib/widgets/toggleable.dart | 2 +- 4 files changed, 154 insertions(+), 30 deletions(-) create mode 100644 packages/flutter/example/widgets/big_switch.dart diff --git a/packages/flutter/example/widgets/big_switch.dart b/packages/flutter/example/widgets/big_switch.dart new file mode 100644 index 00000000000..3cc7de7df41 --- /dev/null +++ b/packages/flutter/example/widgets/big_switch.dart @@ -0,0 +1,35 @@ +// 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:sky/widgets/basic.dart'; +import 'package:sky/widgets/switch.dart'; +import 'package:sky/theme/colors.dart'; +import 'package:vector_math/vector_math.dart'; + +class BigSwitchApp extends App { + bool _value = false; + + void _handleOnChanged(bool value) { + setState(() { + _value = value; + }); + } + + Widget build() { + Matrix4 scale = new Matrix4.identity(); + scale.scale(5.0, 5.0); + return new Container( + child: new Switch(value: _value, onChanged: _handleOnChanged), + padding: new EdgeDims.all(5.0), + transform: scale, + decoration: new BoxDecoration( + backgroundColor: Teal[600] + ) + ); + } +} + +void main() { + runApp(new BigSwitchApp()); +} diff --git a/packages/flutter/lib/widgets/basic.dart b/packages/flutter/lib/widgets/basic.dart index f7df35c8f5a..30ee9577325 100644 --- a/packages/flutter/lib/widgets/basic.dart +++ b/packages/flutter/lib/widgets/basic.dart @@ -24,6 +24,7 @@ export 'package:sky/rendering/flex.dart' show FlexDirection, FlexJustifyContent, export 'package:sky/rendering/object.dart' show Point, Offset, Size, Rect, Color, Paint, Path; export 'package:sky/widgets/widget.dart' show Key, GlobalKey, Widget, Component, StatefulComponent, App, runApp, Listener, ParentDataNode; +typedef void ValueChanged(bool); // PAINTING NODES diff --git a/packages/flutter/lib/widgets/switch.dart b/packages/flutter/lib/widgets/switch.dart index c603a8a8986..f943378fc50 100644 --- a/packages/flutter/lib/widgets/switch.dart +++ b/packages/flutter/lib/widgets/switch.dart @@ -4,14 +4,18 @@ 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'; import 'package:sky/theme/shadows.dart'; import 'package:sky/widgets/basic.dart'; import 'package:sky/widgets/theme.dart'; -import 'package:sky/widgets/toggleable.dart'; +import 'package:sky/widgets/widget.dart'; -export 'package:sky/widgets/toggleable.dart' show ValueChanged; +export 'package:sky/widgets/basic.dart' show ValueChanged; const sky.Color _kThumbOffColor = const sky.Color(0xFFFAFAFA); const sky.Color _kTrackOffColor = const sky.Color(0x42000000); @@ -20,52 +24,136 @@ 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 double _kTrackWidth = + _kSwitchWidth - (_kThumbRadius - _kTrackRadius) * 2.0; +const Duration _kCheckDuration = const Duration(milliseconds: 200); +const Size _kSwitchSize = const Size(_kSwitchWidth + 2.0, _kSwitchHeight + 2.0); -class Switch extends Toggleable { - // TODO(jackson): Hit-test the switch so that it can respond to both taps and swipe gestures +class Switch extends Component { + Switch({Key key, this.value, this.onChanged}) : super(key: key); - Switch({ - Key key, - bool value, - ValueChanged onChanged - }) : super(key: key, value: value, onChanged: onChanged); + final bool value; + final ValueChanged onChanged; - Size get size => const Size(_kSwitchWidth + 2.0, _kSwitchHeight + 2.0); + Widget build() { + return new _SwitchWrapper( + value: value, + onChanged: onChanged, + thumbColor: Theme.of(this).accentColor); + } +} - void customPaintCallback(sky.Canvas canvas, Size size) { +// 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 _SwitchWrapper extends LeafRenderObjectWrapper { + _SwitchWrapper({Key key, this.value, this.onChanged, this.thumbColor}) + : super(key: key); + + final bool value; + final ValueChanged onChanged; + final Color thumbColor; + + _RenderSwitch get root => super.root; + _RenderSwitch createNode() => new _RenderSwitch( + value: value, thumbColor: thumbColor, onChanged: onChanged); + + void syncRenderObject(_SwitchWrapper old) { + super.syncRenderObject(old); + root.value = value; + root.onChanged = onChanged; + root.thumbColor = thumbColor; + } +} + +class _RenderSwitch extends RenderConstrainedBox { + _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); + } + + void handleEvent(sky.Event event, BoxHitTestEntry entry) { + if (event is sky.GestureEvent && + event.type == 'gesturetap') _onChanged(!_value); + } + + 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(); + } + } + + Color _thumbColor; + Color get thumbColor => _thumbColor; + + void set thumbColor(Color value) { + if (value == _thumbColor) return; + _thumbColor = value; + markNeedsPaint(); + } + + ValueChanged _onChanged; + ValueChanged get onChanged => _onChanged; + + void set onChanged(ValueChanged onChanged) { + _onChanged = onChanged; + } + + final AnimatedValue _position = + new AnimatedValue(0.0, end: 1.0); + + AnimationPerformance _performance; + + void paint(PaintingCanvas canvas, Offset offset) { sky.Color thumbColor = _kThumbOffColor; sky.Color trackColor = _kTrackOffColor; - if (value) { - thumbColor = Theme.of(this).accentColor; - trackColor = new sky.Color(thumbColor.value & 0x80FFFFFF); + if (_value) { + thumbColor = _thumbColor; + trackColor = new sky.Color(_thumbColor.value & 0x80FFFFFF); } // Draw the track rrect sky.Paint paint = new sky.Paint()..color = trackColor; paint.setStyle(sky.PaintingStyle.fill); - sky.Rect rect = new sky.Rect.fromLTRB( - 0.0, - _kSwitchHeight / 2.0 - _kTrackHeight / 2.0, - _kTrackWidth, - _kSwitchHeight / 2.0 + _kTrackHeight / 2.0 - ); - sky.RRect rrect = new sky.RRect()..setRectXY(rect, _kTrackRadius, _kTrackRadius); + sky.Rect rect = new sky.Rect.fromLTWH(offset.dx, + offset.dy + _kSwitchHeight / 2.0 - _kTrackHeight / 2.0, _kTrackWidth, + _kTrackHeight); + sky.RRect rrect = new sky.RRect() + ..setRectXY(rect, _kTrackRadius, _kTrackRadius); canvas.drawRRect(rrect, paint); // Draw the raised thumb with a shadow paint.color = thumbColor; var builder = new ShadowDrawLooperBuilder(); - for (BoxShadow boxShadow in shadows[1]) - builder.addShadow(boxShadow.offset, boxShadow.color, boxShadow.blur); + for (BoxShadow boxShadow in shadows[1]) builder.addShadow( + boxShadow.offset, boxShadow.color, boxShadow.blur); paint.setDrawLooper(builder.build()); // The thumb contracts slightly during the animation - double inset = 2.0 - (position.value - 0.5).abs() * 2.0; - Point thumbPos = new Point( - _kTrackRadius + position.value * (_kTrackWidth - _kTrackRadius * 2), - _kSwitchHeight / 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), + offset.dy + _kSwitchHeight / 2.0); canvas.drawCircle(thumbPos, _kThumbRadius - inset, paint); } } diff --git a/packages/flutter/lib/widgets/toggleable.dart b/packages/flutter/lib/widgets/toggleable.dart index 4e196627450..0a02fde3a10 100644 --- a/packages/flutter/lib/widgets/toggleable.dart +++ b/packages/flutter/lib/widgets/toggleable.dart @@ -10,7 +10,7 @@ import 'package:sky/animation/curves.dart'; import 'package:sky/widgets/animated_component.dart'; import 'package:sky/widgets/basic.dart'; -typedef void ValueChanged(bool value); +export 'package:sky/widgets/basic.dart' show ValueChanged; const Duration _kCheckDuration = const Duration(milliseconds: 200);