Commit fc711a10 authored by Adam Barth's avatar Adam Barth Committed by GitHub

Custom switch images are nutty (#4852)

Rather than requiring the developer to specify a full Decoration, we now
just take an ImageProvider for the thumb image. Also, fix
ImageFit.scaleDown to actually work.

Fixes #4571
Fixes #4673
parent d7a4a54b
...@@ -168,11 +168,14 @@ class _SelectionControlsDemoState extends State<SelectionControlsDemo> { ...@@ -168,11 +168,14 @@ class _SelectionControlsDemoState extends State<SelectionControlsDemo> {
child: new Row( child: new Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
new Switch(value: switchValue, onChanged: (bool value) { new Switch(
value: switchValue,
onChanged: (bool value) {
setState(() { setState(() {
switchValue = value; switchValue = value;
}); });
}), }
),
// Disabled switches // Disabled switches
new Switch(value: true, onChanged: null), new Switch(value: true, onChanged: null),
new Switch(value: false, onChanged: null) new Switch(value: false, onChanged: null)
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
...@@ -46,8 +47,8 @@ class Switch extends StatelessWidget { ...@@ -46,8 +47,8 @@ class Switch extends StatelessWidget {
@required this.value, @required this.value,
@required this.onChanged, @required this.onChanged,
this.activeColor, this.activeColor,
this.activeThumbDecoration, this.activeThumbImage,
this.inactiveThumbDecoration this.inactiveThumbImage
}) : super(key: key); }) : super(key: key);
/// Whether this switch is on or off. /// Whether this switch is on or off.
...@@ -67,15 +68,11 @@ class Switch extends StatelessWidget { ...@@ -67,15 +68,11 @@ class Switch extends StatelessWidget {
/// Defaults to accent color of the current [Theme]. /// Defaults to accent color of the current [Theme].
final Color activeColor; final Color activeColor;
/// A decoration to use for the thumb of this switch when the switch is on. /// An image to use on the thumb of this switch when the switch is on.
/// final ImageProvider activeThumbImage;
/// Defaults to a circular piece of material.
final Decoration activeThumbDecoration;
/// A decoration to use for the thumb of this switch when the switch is off. /// An image to use on the thumb of this switch when the switch is off.
/// final ImageProvider inactiveThumbImage;
/// Defaults to a circular piece of material.
final Decoration inactiveThumbDecoration;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
...@@ -100,8 +97,8 @@ class Switch extends StatelessWidget { ...@@ -100,8 +97,8 @@ class Switch extends StatelessWidget {
value: value, value: value,
activeColor: activeThumbColor, activeColor: activeThumbColor,
inactiveColor: inactiveThumbColor, inactiveColor: inactiveThumbColor,
activeThumbDecoration: activeThumbDecoration, activeThumbImage: activeThumbImage,
inactiveThumbDecoration: inactiveThumbDecoration, inactiveThumbImage: inactiveThumbImage,
activeTrackColor: activeTrackColor, activeTrackColor: activeTrackColor,
inactiveTrackColor: inactiveTrackColor, inactiveTrackColor: inactiveTrackColor,
configuration: createLocalImageConfiguration(context), configuration: createLocalImageConfiguration(context),
...@@ -124,8 +121,8 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -124,8 +121,8 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
this.value, this.value,
this.activeColor, this.activeColor,
this.inactiveColor, this.inactiveColor,
this.activeThumbDecoration, this.activeThumbImage,
this.inactiveThumbDecoration, this.inactiveThumbImage,
this.activeTrackColor, this.activeTrackColor,
this.inactiveTrackColor, this.inactiveTrackColor,
this.configuration, this.configuration,
...@@ -135,8 +132,8 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -135,8 +132,8 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
final bool value; final bool value;
final Color activeColor; final Color activeColor;
final Color inactiveColor; final Color inactiveColor;
final Decoration activeThumbDecoration; final ImageProvider activeThumbImage;
final Decoration inactiveThumbDecoration; final ImageProvider inactiveThumbImage;
final Color activeTrackColor; final Color activeTrackColor;
final Color inactiveTrackColor; final Color inactiveTrackColor;
final ImageConfiguration configuration; final ImageConfiguration configuration;
...@@ -147,8 +144,8 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -147,8 +144,8 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
value: value, value: value,
activeColor: activeColor, activeColor: activeColor,
inactiveColor: inactiveColor, inactiveColor: inactiveColor,
activeThumbDecoration: activeThumbDecoration, activeThumbImage: activeThumbImage,
inactiveThumbDecoration: inactiveThumbDecoration, inactiveThumbImage: inactiveThumbImage,
activeTrackColor: activeTrackColor, activeTrackColor: activeTrackColor,
inactiveTrackColor: inactiveTrackColor, inactiveTrackColor: inactiveTrackColor,
configuration: configuration, configuration: configuration,
...@@ -161,8 +158,8 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -161,8 +158,8 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
..value = value ..value = value
..activeColor = activeColor ..activeColor = activeColor
..inactiveColor = inactiveColor ..inactiveColor = inactiveColor
..activeThumbDecoration = activeThumbDecoration ..activeThumbImage = activeThumbImage
..inactiveThumbDecoration = inactiveThumbDecoration ..inactiveThumbImage = inactiveThumbImage
..activeTrackColor = activeTrackColor ..activeTrackColor = activeTrackColor
..inactiveTrackColor = inactiveTrackColor ..inactiveTrackColor = inactiveTrackColor
..configuration = configuration ..configuration = configuration
...@@ -182,14 +179,14 @@ class _RenderSwitch extends RenderToggleable { ...@@ -182,14 +179,14 @@ class _RenderSwitch extends RenderToggleable {
bool value, bool value,
Color activeColor, Color activeColor,
Color inactiveColor, Color inactiveColor,
Decoration activeThumbDecoration, ImageProvider activeThumbImage,
Decoration inactiveThumbDecoration, ImageProvider inactiveThumbImage,
Color activeTrackColor, Color activeTrackColor,
Color inactiveTrackColor, Color inactiveTrackColor,
ImageConfiguration configuration, ImageConfiguration configuration,
ValueChanged<bool> onChanged ValueChanged<bool> onChanged
}) : _activeThumbDecoration = activeThumbDecoration, }) : _activeThumbImage = activeThumbImage,
_inactiveThumbDecoration = inactiveThumbDecoration, _inactiveThumbImage = inactiveThumbImage,
_activeTrackColor = activeTrackColor, _activeTrackColor = activeTrackColor,
_inactiveTrackColor = inactiveTrackColor, _inactiveTrackColor = inactiveTrackColor,
_configuration = configuration, _configuration = configuration,
...@@ -206,21 +203,21 @@ class _RenderSwitch extends RenderToggleable { ...@@ -206,21 +203,21 @@ class _RenderSwitch extends RenderToggleable {
..onEnd = _handleDragEnd; ..onEnd = _handleDragEnd;
} }
Decoration get activeThumbDecoration => _activeThumbDecoration; ImageProvider get activeThumbImage => _activeThumbImage;
Decoration _activeThumbDecoration; ImageProvider _activeThumbImage;
set activeThumbDecoration(Decoration value) { set activeThumbImage(ImageProvider value) {
if (value == _activeThumbDecoration) if (value == _activeThumbImage)
return; return;
_activeThumbDecoration = value; _activeThumbImage = value;
markNeedsPaint(); markNeedsPaint();
} }
Decoration get inactiveThumbDecoration => _inactiveThumbDecoration; ImageProvider get inactiveThumbImage => _inactiveThumbImage;
Decoration _inactiveThumbDecoration; ImageProvider _inactiveThumbImage;
set inactiveThumbDecoration(Decoration value) { set inactiveThumbImage(ImageProvider value) {
if (value == _inactiveThumbDecoration) if (value == _inactiveThumbImage)
return; return;
_inactiveThumbDecoration = value; _inactiveThumbImage = value;
markNeedsPaint(); markNeedsPaint();
} }
...@@ -295,16 +292,29 @@ class _RenderSwitch extends RenderToggleable { ...@@ -295,16 +292,29 @@ class _RenderSwitch extends RenderToggleable {
} }
Color _cachedThumbColor; Color _cachedThumbColor;
ImageProvider _cachedThumbImage;
BoxPainter _cachedThumbPainter; BoxPainter _cachedThumbPainter;
BoxDecoration _createDefaultThumbDecoration(Color color) { BoxDecoration _createDefaultThumbDecoration(Color color, ImageProvider image) {
return new BoxDecoration( return new BoxDecoration(
backgroundColor: color, backgroundColor: color,
backgroundImage: image == null ? null : new BackgroundImage(image: image),
shape: BoxShape.circle, shape: BoxShape.circle,
boxShadow: kElevationToShadow[1] 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 @override
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
final Canvas canvas = context.canvas; final Canvas canvas = context.canvas;
...@@ -334,19 +344,17 @@ class _RenderSwitch extends RenderToggleable { ...@@ -334,19 +344,17 @@ class _RenderSwitch extends RenderToggleable {
paintRadialReaction(canvas, offset, thumbPosition); paintRadialReaction(canvas, offset, thumbPosition);
try {
_isPainting = true;
BoxPainter thumbPainter; BoxPainter thumbPainter;
if (_inactiveThumbDecoration == null && _activeThumbDecoration == null) {
final Color thumbColor = isActive ? Color.lerp(inactiveColor, activeColor, currentPosition) : inactiveColor; 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; _cachedThumbColor = thumbColor;
_cachedThumbPainter = _createDefaultThumbDecoration(thumbColor).createBoxPainter(markNeedsPaint); _cachedThumbImage = thumbImage;
_cachedThumbPainter = _createDefaultThumbDecoration(thumbColor, thumbImage).createBoxPainter(_handleDecorationChanged);
} }
thumbPainter = _cachedThumbPainter; 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 // The thumb contracts slightly during the animation
final double inset = 1.0 - (currentPosition - 0.5).abs() * 2.0; final double inset = 1.0 - (currentPosition - 0.5).abs() * 2.0;
...@@ -356,5 +364,8 @@ class _RenderSwitch extends RenderToggleable { ...@@ -356,5 +364,8 @@ class _RenderSwitch extends RenderToggleable {
thumbPosition.toOffset() + offset - new Offset(radius, radius), thumbPosition.toOffset() + offset - new Offset(radius, radius),
configuration.copyWith(size: new Size.fromRadius(radius)) configuration.copyWith(size: new Size.fromRadius(radius))
); );
} finally {
_isPainting = false;
}
} }
} }
...@@ -864,11 +864,12 @@ void paintImage({ ...@@ -864,11 +864,12 @@ void paintImage({
break; break;
case ImageFit.scaleDown: case ImageFit.scaleDown:
sourceSize = inputSize; sourceSize = inputSize;
destinationSize = outputSize; destinationSize = inputSize;
if (sourceSize.height > destinationSize.height) final double aspectRatio = inputSize.width / inputSize.height;
destinationSize = new Size(sourceSize.width * destinationSize.height / sourceSize.height, sourceSize.height); if (destinationSize.height > outputSize.height)
if (sourceSize.width > destinationSize.width) destinationSize = new Size(outputSize.height * aspectRatio, outputSize.height);
destinationSize = new Size(destinationSize.width, sourceSize.height * destinationSize.width / sourceSize.width); if (destinationSize.width > outputSize.width)
destinationSize = new Size(outputSize.width, outputSize.height / aspectRatio);
break; break;
} }
if (centerSlice != null) { if (centerSlice != null) {
......
...@@ -71,45 +71,4 @@ void main() { ...@@ -71,45 +71,4 @@ void main() {
await tester.tap(find.byKey(switchKey)); await tester.tap(find.byKey(switchKey));
expect(value, isTrue); 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);
});
} }
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