Commit 3176921d authored by Adam Barth's avatar Adam Barth Committed by GitHub

Add CupertinoSlider (#7336)

This widget matches the style of the iOS slider widget. We don't yet
have the proper visual design for the disabled state.
parent 96fa6c3e
...@@ -7,4 +7,6 @@ ...@@ -7,4 +7,6 @@
/// To use, import `package:flutter/cupertino.dart`. /// To use, import `package:flutter/cupertino.dart`.
library cupertino; library cupertino;
export 'src/cupertino/slider.dart';
export 'src/cupertino/switch.dart'; export 'src/cupertino/switch.dart';
export 'src/cupertino/thumb_painter.dart';
This diff is collapsed.
...@@ -10,6 +10,8 @@ import 'package:flutter/rendering.dart'; ...@@ -10,6 +10,8 @@ import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'thumb_painter.dart';
/// An iOS-style switch. /// An iOS-style switch.
/// ///
/// Used to toggle the on/off state of a single setting. /// Used to toggle the on/off state of a single setting.
...@@ -118,24 +120,18 @@ class _CupertinoSwitchRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -118,24 +120,18 @@ class _CupertinoSwitchRenderObjectWidget extends LeafRenderObjectWidget {
} }
} }
const double _kTrackWidth = 51.0; const double _kTrackWidth = 51.0;
const double _kTrackHeight = 31.0; const double _kTrackHeight = 31.0;
const double _kTrackRadius = _kTrackHeight / 2.0; const double _kTrackRadius = _kTrackHeight / 2.0;
const double _kTrackInnerStart = _kTrackHeight / 2.0; const double _kTrackInnerStart = _kTrackHeight / 2.0;
const double _kTrackInnerEnd = _kTrackWidth - _kTrackInnerStart; const double _kTrackInnerEnd = _kTrackWidth - _kTrackInnerStart;
const double _kTrackInnerLength = _kTrackInnerEnd - _kTrackInnerStart; const double _kTrackInnerLength = _kTrackInnerEnd - _kTrackInnerStart;
const double _kThumbRadius = 14.0;
const double _kThumbExtension = 7.0;
const double _kSwitchWidth = 59.0; const double _kSwitchWidth = 59.0;
const double _kSwitchHeight = 39.0; const double _kSwitchHeight = 39.0;
const Color _kTrackColor = const Color(0xFFE5E5E5); const Color _kTrackColor = const Color(0xFFE5E5E5);
const Color _kThumbColor = const Color(0xFFFFFFFF);
const Color _kThumbShadowColor = const Color(0x2C000000);
const Duration _kReactionDuration = const Duration(milliseconds: 300); const Duration _kReactionDuration = const Duration(milliseconds: 300);
const Duration _kToggleDuration = const Duration(milliseconds: 200); const Duration _kToggleDuration = const Duration(milliseconds: 200);
final MaskFilter _kShadowMaskFilter = new MaskFilter.blur(BlurStyle.normal, BoxShadow.convertRadiusToSigma(1.0));
class _RenderCupertinoSwitch extends RenderConstrainedBox implements SemanticsActionHandler { class _RenderCupertinoSwitch extends RenderConstrainedBox implements SemanticsActionHandler {
_RenderCupertinoSwitch({ _RenderCupertinoSwitch({
...@@ -355,6 +351,8 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox implements SemanticsAc ...@@ -355,6 +351,8 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox implements SemanticsAc
_handleTap(); _handleTap();
} }
final CupertinoThumbPainter _thumbPainter = new CupertinoThumbPainter();
@override @override
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
final Canvas canvas = context.canvas; final Canvas canvas = context.canvas;
...@@ -378,33 +376,25 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox implements SemanticsAc ...@@ -378,33 +376,25 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox implements SemanticsAc
final RRect innerRRect = new RRect.fromRectAndRadius(trackRect.deflate(borderThickness), const Radius.circular(_kTrackRadius)); final RRect innerRRect = new RRect.fromRectAndRadius(trackRect.deflate(borderThickness), const Radius.circular(_kTrackRadius));
canvas.drawDRRect(outerRRect, innerRRect, paint); canvas.drawDRRect(outerRRect, innerRRect, paint);
final double currentThumbExtension = _kThumbExtension * currentReactionValue; final double currentThumbExtension = CupertinoThumbPainter.extension * currentReactionValue;
final double thumbLeft = lerpDouble( final double thumbLeft = lerpDouble(
trackRect.left + _kTrackInnerStart - _kThumbRadius, trackRect.left + _kTrackInnerStart - CupertinoThumbPainter.radius,
trackRect.left + _kTrackInnerEnd - _kThumbRadius - currentThumbExtension, trackRect.left + _kTrackInnerEnd - CupertinoThumbPainter.radius - currentThumbExtension,
currentPosition, currentPosition,
); );
final double thumbRight = lerpDouble( final double thumbRight = lerpDouble(
trackRect.left + _kTrackInnerStart + _kThumbRadius + currentThumbExtension, trackRect.left + _kTrackInnerStart + CupertinoThumbPainter.radius + currentThumbExtension,
trackRect.left + _kTrackInnerEnd + _kThumbRadius, trackRect.left + _kTrackInnerEnd + CupertinoThumbPainter.radius,
currentPosition, currentPosition,
); );
final double thumbCenterY = offset.dy + size.height / 2.0; final double thumbCenterY = offset.dy + size.height / 2.0;
final RRect thumbRRect = new RRect.fromRectAndRadius(new Rect.fromLTRB( _thumbPainter.paint(canvas, new Rect.fromLTRB(
thumbLeft, thumbCenterY - _kThumbRadius, thumbRight, thumbCenterY + _kThumbRadius thumbLeft,
), const Radius.circular(_kThumbRadius)); thumbCenterY - CupertinoThumbPainter.radius,
thumbRight,
paint thumbCenterY + CupertinoThumbPainter.radius,
..color = _kThumbShadowColor ));
..maskFilter = _kShadowMaskFilter;
canvas.drawRRect(thumbRRect, paint);
canvas.drawRRect(thumbRRect.shift(const Offset(0.0, 3.0)), paint);
paint
..color = _kThumbColor
..maskFilter = null;
canvas.drawRRect(thumbRRect, paint);
} }
@override @override
......
// Copyright 2017 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/painting.dart';
final MaskFilter _kShadowMaskFilter = new MaskFilter.blur(BlurStyle.normal, BoxShadow.convertRadiusToSigma(1.0));
class CupertinoThumbPainter {
CupertinoThumbPainter({
this.color: const Color(0xFFFFFFFF),
this.shadowColor: const Color(0x2C000000),
});
final Color color;
final Color shadowColor;
static const double radius = 14.0;
static const double extension = 7.0;
void paint(Canvas canvas, Rect rect) {
final RRect rrect = new RRect.fromRectAndRadius(rect, new Radius.circular(rect.shortestSide / 2.0));
Paint paint = new Paint()
..color = shadowColor
..maskFilter = _kShadowMaskFilter;
canvas.drawRRect(rrect, paint);
canvas.drawRRect(rrect.shift(const Offset(0.0, 3.0)), paint);
paint
..color = color
..maskFilter = null;
canvas.drawRRect(rrect, paint);
}
}
...@@ -355,7 +355,6 @@ class _RenderSlider extends RenderConstrainedBox implements SemanticsActionHandl ...@@ -355,7 +355,6 @@ class _RenderSlider extends RenderConstrainedBox implements SemanticsActionHandl
_currentDragValue = (globalToLocal(details.globalPosition).x - _kReactionRadius) / _trackLength; _currentDragValue = (globalToLocal(details.globalPosition).x - _kReactionRadius) / _trackLength;
onChanged(_discretizedCurrentDragValue); onChanged(_discretizedCurrentDragValue);
_reactionController.forward(); _reactionController.forward();
markNeedsPaint();
} }
} }
...@@ -371,7 +370,6 @@ class _RenderSlider extends RenderConstrainedBox implements SemanticsActionHandl ...@@ -371,7 +370,6 @@ class _RenderSlider extends RenderConstrainedBox implements SemanticsActionHandl
_active = false; _active = false;
_currentDragValue = 0.0; _currentDragValue = 0.0;
_reactionController.reverse(); _reactionController.reverse();
markNeedsPaint();
} }
} }
...@@ -490,14 +488,15 @@ class _RenderSlider extends RenderConstrainedBox implements SemanticsActionHandl ...@@ -490,14 +488,15 @@ class _RenderSlider extends RenderConstrainedBox implements SemanticsActionHandl
@override @override
void performAction(SemanticsAction action) { void performAction(SemanticsAction action) {
final double unit = divisions != null ? 1.0 / divisions : _kAdjustmentUnit;
switch (action) { switch (action) {
case SemanticsAction.increase: case SemanticsAction.increase:
if (isInteractive) if (isInteractive)
onChanged((value + _kAdjustmentUnit).clamp(0.0, 1.0)); onChanged((value + unit).clamp(0.0, 1.0));
break; break;
case SemanticsAction.decrease: case SemanticsAction.decrease:
if (isInteractive) if (isInteractive)
onChanged((value - _kAdjustmentUnit).clamp(0.0, 1.0)); onChanged((value - unit).clamp(0.0, 1.0));
break; break;
default: default:
assert(false); assert(false);
......
// Copyright 2016 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/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Slider does not move when tapped', (WidgetTester tester) async {
final Key sliderKey = new UniqueKey();
double value = 0.0;
await tester.pumpWidget(
new StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return new Material(
child: new Center(
child: new CupertinoSlider(
key: sliderKey,
value: value,
onChanged: (double newValue) {
setState(() {
value = newValue;
});
},
),
),
);
},
),
);
expect(value, equals(0.0));
await tester.tap(find.byKey(sliderKey));
expect(value, equals(0.0));
await tester.pump(); // No animation should start.
// Check the transientCallbackCount before tearing down the widget to ensure
// that no animation is running.
expect(SchedulerBinding.instance.transientCallbackCount, equals(0));
});
testWidgets('Slider moves when dragged', (WidgetTester tester) async {
final Key sliderKey = new UniqueKey();
double value = 0.0;
await tester.pumpWidget(
new StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return new Material(
child: new Center(
child: new CupertinoSlider(
key: sliderKey,
value: value,
onChanged: (double newValue) {
setState(() {
value = newValue;
});
},
),
),
);
},
),
);
expect(value, equals(0.0));
final Point topLeft = tester.getTopLeft(find.byKey(sliderKey));
const double unit = CupertinoThumbPainter.radius;
const double delta = 3.0 * unit;
await tester.scrollAt(
topLeft + const Offset(unit, unit), const Offset(delta, 0.0));
final Size size = tester.getSize(find.byKey(sliderKey));
expect(value, equals(delta / (size.width - 2.0 * (8.0 + CupertinoThumbPainter.radius))));
await tester.pump(); // No animation should start.
// Check the transientCallbackCount before tearing down the widget to ensure
// that no animation is running.
expect(SchedulerBinding.instance.transientCallbackCount, equals(0));
});
}
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