diff --git a/examples/flutter_gallery/lib/demo/selection_controls_demo.dart b/examples/flutter_gallery/lib/demo/selection_controls_demo.dart index a3d4e74bb6..9fe17644c8 100644 --- a/examples/flutter_gallery/lib/demo/selection_controls_demo.dart +++ b/examples/flutter_gallery/lib/demo/selection_controls_demo.dart @@ -168,11 +168,14 @@ class _SelectionControlsDemoState extends State { child: new Row( mainAxisSize: MainAxisSize.min, children: [ - new Switch(value: switchValue, onChanged: (bool value) { - setState(() { + new Switch( + value: switchValue, + onChanged: (bool value) { + setState(() { switchValue = value; - }); - }), + }); + } + ), // Disabled switches new Switch(value: true, onChanged: null), new Switch(value: false, onChanged: null) diff --git a/packages/flutter/lib/src/material/switch.dart b/packages/flutter/lib/src/material/switch.dart index c06aca46e7..183008c0bd 100644 --- a/packages/flutter/lib/src/material/switch.dart +++ b/packages/flutter/lib/src/material/switch.dart @@ -4,6 +4,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:meta/meta.dart'; @@ -46,8 +47,8 @@ class Switch extends StatelessWidget { @required this.value, @required this.onChanged, this.activeColor, - this.activeThumbDecoration, - this.inactiveThumbDecoration + this.activeThumbImage, + this.inactiveThumbImage }) : super(key: key); /// Whether this switch is on or off. @@ -67,15 +68,11 @@ class Switch extends StatelessWidget { /// Defaults to accent color of the current [Theme]. final Color activeColor; - /// A decoration to use for the thumb of this switch when the switch is on. - /// - /// Defaults to a circular piece of material. - final Decoration activeThumbDecoration; + /// An image to use on the thumb of this switch when the switch is on. + final ImageProvider activeThumbImage; - /// A decoration to use for the thumb of this switch when the switch is off. - /// - /// Defaults to a circular piece of material. - final Decoration inactiveThumbDecoration; + /// An image to use on the thumb of this switch when the switch is off. + final ImageProvider inactiveThumbImage; @override Widget build(BuildContext context) { @@ -100,8 +97,8 @@ class Switch extends StatelessWidget { value: value, activeColor: activeThumbColor, inactiveColor: inactiveThumbColor, - activeThumbDecoration: activeThumbDecoration, - inactiveThumbDecoration: inactiveThumbDecoration, + activeThumbImage: activeThumbImage, + inactiveThumbImage: inactiveThumbImage, activeTrackColor: activeTrackColor, inactiveTrackColor: inactiveTrackColor, configuration: createLocalImageConfiguration(context), @@ -124,8 +121,8 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget { this.value, this.activeColor, this.inactiveColor, - this.activeThumbDecoration, - this.inactiveThumbDecoration, + this.activeThumbImage, + this.inactiveThumbImage, this.activeTrackColor, this.inactiveTrackColor, this.configuration, @@ -135,8 +132,8 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget { final bool value; final Color activeColor; final Color inactiveColor; - final Decoration activeThumbDecoration; - final Decoration inactiveThumbDecoration; + final ImageProvider activeThumbImage; + final ImageProvider inactiveThumbImage; final Color activeTrackColor; final Color inactiveTrackColor; final ImageConfiguration configuration; @@ -147,8 +144,8 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget { value: value, activeColor: activeColor, inactiveColor: inactiveColor, - activeThumbDecoration: activeThumbDecoration, - inactiveThumbDecoration: inactiveThumbDecoration, + activeThumbImage: activeThumbImage, + inactiveThumbImage: inactiveThumbImage, activeTrackColor: activeTrackColor, inactiveTrackColor: inactiveTrackColor, configuration: configuration, @@ -161,8 +158,8 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget { ..value = value ..activeColor = activeColor ..inactiveColor = inactiveColor - ..activeThumbDecoration = activeThumbDecoration - ..inactiveThumbDecoration = inactiveThumbDecoration + ..activeThumbImage = activeThumbImage + ..inactiveThumbImage = inactiveThumbImage ..activeTrackColor = activeTrackColor ..inactiveTrackColor = inactiveTrackColor ..configuration = configuration @@ -182,14 +179,14 @@ class _RenderSwitch extends RenderToggleable { bool value, Color activeColor, Color inactiveColor, - Decoration activeThumbDecoration, - Decoration inactiveThumbDecoration, + ImageProvider activeThumbImage, + ImageProvider inactiveThumbImage, Color activeTrackColor, Color inactiveTrackColor, ImageConfiguration configuration, ValueChanged onChanged - }) : _activeThumbDecoration = activeThumbDecoration, - _inactiveThumbDecoration = inactiveThumbDecoration, + }) : _activeThumbImage = activeThumbImage, + _inactiveThumbImage = inactiveThumbImage, _activeTrackColor = activeTrackColor, _inactiveTrackColor = inactiveTrackColor, _configuration = configuration, @@ -206,21 +203,21 @@ class _RenderSwitch extends RenderToggleable { ..onEnd = _handleDragEnd; } - Decoration get activeThumbDecoration => _activeThumbDecoration; - Decoration _activeThumbDecoration; - set activeThumbDecoration(Decoration value) { - if (value == _activeThumbDecoration) + ImageProvider get activeThumbImage => _activeThumbImage; + ImageProvider _activeThumbImage; + set activeThumbImage(ImageProvider value) { + if (value == _activeThumbImage) return; - _activeThumbDecoration = value; + _activeThumbImage = value; markNeedsPaint(); } - Decoration get inactiveThumbDecoration => _inactiveThumbDecoration; - Decoration _inactiveThumbDecoration; - set inactiveThumbDecoration(Decoration value) { - if (value == _inactiveThumbDecoration) + ImageProvider get inactiveThumbImage => _inactiveThumbImage; + ImageProvider _inactiveThumbImage; + set inactiveThumbImage(ImageProvider value) { + if (value == _inactiveThumbImage) return; - _inactiveThumbDecoration = value; + _inactiveThumbImage = value; markNeedsPaint(); } @@ -295,16 +292,29 @@ class _RenderSwitch extends RenderToggleable { } Color _cachedThumbColor; + ImageProvider _cachedThumbImage; BoxPainter _cachedThumbPainter; - BoxDecoration _createDefaultThumbDecoration(Color color) { + BoxDecoration _createDefaultThumbDecoration(Color color, ImageProvider image) { return new BoxDecoration( backgroundColor: color, + backgroundImage: image == null ? null : new BackgroundImage(image: image), shape: BoxShape.circle, boxShadow: kElevationToShadow[1] ); } + bool _isPainting = false; + + void _handleDecorationChanged() { + // If the image decoration is available synchronously, we'll get called here + // during paint. There's no reason to mark ourselves as needing paint if we + // are already in the middle of painting. (In fact, doing so would trigger + // an assert). + if (!_isPainting) + markNeedsPaint(); + } + @override void paint(PaintingContext context, Offset offset) { final Canvas canvas = context.canvas; @@ -334,27 +344,28 @@ class _RenderSwitch extends RenderToggleable { paintRadialReaction(canvas, offset, thumbPosition); - BoxPainter thumbPainter; - if (_inactiveThumbDecoration == null && _activeThumbDecoration == null) { + try { + _isPainting = true; + BoxPainter thumbPainter; final Color thumbColor = isActive ? Color.lerp(inactiveColor, activeColor, currentPosition) : inactiveColor; - if (thumbColor != _cachedThumbColor || _cachedThumbPainter == null) { + final ImageProvider thumbImage = isActive ? (currentPosition < 0.5 ? inactiveThumbImage : activeThumbImage) : inactiveThumbImage; + if (_cachedThumbPainter == null || thumbColor != _cachedThumbColor || thumbImage != _cachedThumbImage) { _cachedThumbColor = thumbColor; - _cachedThumbPainter = _createDefaultThumbDecoration(thumbColor).createBoxPainter(markNeedsPaint); + _cachedThumbImage = thumbImage; + _cachedThumbPainter = _createDefaultThumbDecoration(thumbColor, thumbImage).createBoxPainter(_handleDecorationChanged); } thumbPainter = _cachedThumbPainter; - } else { - final Decoration startDecoration = _inactiveThumbDecoration ?? _createDefaultThumbDecoration(inactiveColor); - final Decoration endDecoration = _activeThumbDecoration ?? _createDefaultThumbDecoration(isActive ? activeTrackColor : inactiveColor); - thumbPainter = Decoration.lerp(startDecoration, endDecoration, currentPosition).createBoxPainter(markNeedsPaint); - } - // The thumb contracts slightly during the animation - final double inset = 1.0 - (currentPosition - 0.5).abs() * 2.0; - final double radius = _kThumbRadius - inset; - thumbPainter.paint( - canvas, - thumbPosition.toOffset() + offset - new Offset(radius, radius), - configuration.copyWith(size: new Size.fromRadius(radius)) - ); + // The thumb contracts slightly during the animation + final double inset = 1.0 - (currentPosition - 0.5).abs() * 2.0; + final double radius = _kThumbRadius - inset; + thumbPainter.paint( + canvas, + thumbPosition.toOffset() + offset - new Offset(radius, radius), + configuration.copyWith(size: new Size.fromRadius(radius)) + ); + } finally { + _isPainting = false; + } } } diff --git a/packages/flutter/lib/src/painting/box_painter.dart b/packages/flutter/lib/src/painting/box_painter.dart index 0bf33e01e3..ee81b86ba2 100644 --- a/packages/flutter/lib/src/painting/box_painter.dart +++ b/packages/flutter/lib/src/painting/box_painter.dart @@ -864,11 +864,12 @@ void paintImage({ break; case ImageFit.scaleDown: sourceSize = inputSize; - destinationSize = outputSize; - if (sourceSize.height > destinationSize.height) - destinationSize = new Size(sourceSize.width * destinationSize.height / sourceSize.height, sourceSize.height); - if (sourceSize.width > destinationSize.width) - destinationSize = new Size(destinationSize.width, sourceSize.height * destinationSize.width / sourceSize.width); + destinationSize = inputSize; + final double aspectRatio = inputSize.width / inputSize.height; + if (destinationSize.height > outputSize.height) + destinationSize = new Size(outputSize.height * aspectRatio, outputSize.height); + if (destinationSize.width > outputSize.width) + destinationSize = new Size(outputSize.width, outputSize.height / aspectRatio); break; } if (centerSlice != null) { diff --git a/packages/flutter/test/material/switch_test.dart b/packages/flutter/test/material/switch_test.dart index 5ff4c6c4da..f0e831dad4 100644 --- a/packages/flutter/test/material/switch_test.dart +++ b/packages/flutter/test/material/switch_test.dart @@ -71,45 +71,4 @@ void main() { await tester.tap(find.byKey(switchKey)); expect(value, isTrue); }); - - testWidgets('Switch listens to the decorations it paints', (WidgetTester tester) async { - TestDecoration activeDecoration = new TestDecoration(); - TestDecoration inactiveDecoration = new TestDecoration(); - - Widget build(bool active, TestDecoration activeDecoration, TestDecoration inactiveDecoration) { - return new Material( - child: new Center( - child: new Switch( - value: active, - onChanged: null, - activeThumbDecoration: activeDecoration, - inactiveThumbDecoration: inactiveDecoration - ) - ) - ); - } - - // no build yet - expect(activeDecoration.listeners, 0); - expect(inactiveDecoration.listeners, 0); - - await tester.pumpWidget(build(false, activeDecoration, inactiveDecoration)); - expect(activeDecoration.listeners, 0); - expect(inactiveDecoration.listeners, 1); - - await tester.pumpWidget(build(true, activeDecoration, inactiveDecoration)); - // started the animation, but we're on frame 0 - expect(activeDecoration.listeners, 0); - expect(inactiveDecoration.listeners, 2); - - await tester.pump(const Duration(milliseconds: 30)); // slightly into the animation - // we're painting some lerped decoration that doesn't exactly match either - expect(activeDecoration.listeners, 0); - expect(inactiveDecoration.listeners, 2); - - await tester.pump(const Duration(seconds: 1)); // ended animation - expect(activeDecoration.listeners, 1); - expect(inactiveDecoration.listeners, 2); - - }); }