Unverified Commit 4815b26d authored by LongCatIsLooong's avatar LongCatIsLooong Committed by GitHub

Dark mode for CupertinoSwitch and CupertinoScrollbar, Fidelity updates (#40636)

parent d10034e9
ae7615ce466a548b41a0f7b9860ec453646f73e9
0728c1b6e968602e173d0153a88d9cfb932d8c9b
......@@ -1014,14 +1014,14 @@ const CupertinoSystemColorsData _kSystemColorsFallback = CupertinoSystemColorsDa
darkHighContrastElevatedColor: Color.fromARGB(112, 120, 120, 128),
),
secondarySystemFill: CupertinoDynamicColor(
color: Color.fromARGB(153, 60, 60, 67),
darkColor: Color.fromARGB(153, 235, 235, 245),
highContrastColor: Color.fromARGB(173, 60, 60, 67),
darkHighContrastColor: Color.fromARGB(173, 235, 235, 245),
elevatedColor: Color.fromARGB(153, 60, 60, 67),
darkElevatedColor: Color.fromARGB(153, 235, 235, 245),
highContrastElevatedColor: Color.fromARGB(173, 60, 60, 67),
darkHighContrastElevatedColor: Color.fromARGB(173, 235, 235, 245),
color: Color.fromARGB(40, 120, 120, 128),
darkColor: Color.fromARGB(81, 120, 120, 128),
highContrastColor: Color.fromARGB(61, 120, 120, 128),
darkHighContrastColor: Color.fromARGB(102, 120, 120, 128),
elevatedColor: Color.fromARGB(40, 120, 120, 128),
darkElevatedColor: Color.fromARGB(81, 120, 120, 128),
highContrastElevatedColor: Color.fromARGB(61, 120, 120, 128),
darkHighContrastElevatedColor: Color.fromARGB(102, 120, 120, 128),
),
tertiarySystemFill: CupertinoDynamicColor(
color: Color.fromARGB(30, 118, 118, 128),
......
......@@ -8,19 +8,25 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'colors.dart';
// All values eyeballed.
const Color _kScrollbarColor = Color(0x99777777);
const double _kScrollbarMinLength = 36.0;
const double _kScrollbarMinOverscrollLength = 8.0;
const Radius _kScrollbarRadius = Radius.circular(1.5);
const Radius _kScrollbarRadiusDragging = Radius.circular(4.0);
const Duration _kScrollbarTimeToFade = Duration(milliseconds: 1200);
const Duration _kScrollbarFadeDuration = Duration(milliseconds: 250);
const Duration _kScrollbarResizeDuration = Duration(milliseconds: 150);
// These values are measured using screenshots from an iPhone XR 13.0 simulator.
const double _kScrollbarThickness = 2.5;
// Extracted from iOS 13.1 beta using Debug View Hierarchy.
const Color _kScrollbarColor = CupertinoDynamicColor.withBrightness(
color: Color(0x59000000),
darkColor: Color(0x80FFFFFF),
);
const double _kScrollbarThickness = 3;
const double _kScrollbarThicknessDragging = 8.0;
const Radius _kScrollbarRadius = Radius.circular(1.5);
const Radius _kScrollbarRadiusDragging = Radius.circular(4.0);
// This is the amount of space from the top of a vertical scrollbar to the
// top edge of the scrollable, measured when the vertical scrollbar overscrolls
// to the top.
......@@ -101,7 +107,6 @@ class CupertinoScrollbar extends StatefulWidget {
class _CupertinoScrollbarState extends State<CupertinoScrollbar> with TickerProviderStateMixin {
final GlobalKey _customPaintKey = GlobalKey();
ScrollbarPainter _painter;
TextDirection _textDirection;
AnimationController _fadeoutAnimationController;
Animation<double> _fadeoutOpacityAnimation;
......@@ -141,15 +146,22 @@ class _CupertinoScrollbarState extends State<CupertinoScrollbar> with TickerProv
@override
void didChangeDependencies() {
super.didChangeDependencies();
_textDirection = Directionality.of(context);
_painter = _buildCupertinoScrollbarPainter();
if (_painter == null) {
_painter = _buildCupertinoScrollbarPainter(context);
}
else {
_painter
..textDirection = Directionality.of(context)
..color = CupertinoDynamicColor.resolve(_kScrollbarColor, context)
..padding = MediaQuery.of(context).padding;
}
}
/// Returns a [ScrollbarPainter] visually styled like the iOS scrollbar.
ScrollbarPainter _buildCupertinoScrollbarPainter() {
ScrollbarPainter _buildCupertinoScrollbarPainter(BuildContext context) {
return ScrollbarPainter(
color: _kScrollbarColor,
textDirection: _textDirection,
color: CupertinoDynamicColor.resolve(_kScrollbarColor, context),
textDirection: Directionality.of(context),
thickness: _thickness,
fadeoutOpacityAnimation: _fadeoutOpacityAnimation,
mainAxisMargin: _kScrollbarMainAxisMargin,
......@@ -353,9 +365,7 @@ class _CupertinoScrollbarState extends State<CupertinoScrollbar> with TickerProv
child: CustomPaint(
key: _customPaintKey,
foregroundPainter: _painter,
child: RepaintBoundary(
child: widget.child,
),
child: RepaintBoundary(child: widget.child),
),
),
),
......
......@@ -474,8 +474,6 @@ class _RenderCupertinoSlider extends RenderConstrainedBox {
_drag.addPointer(event);
}
final CupertinoThumbPainter _thumbPainter = CupertinoThumbPainter();
@override
void paint(PaintingContext context, Offset offset) {
double visualPosition;
......@@ -514,7 +512,7 @@ class _RenderCupertinoSlider extends RenderConstrainedBox {
}
final Offset thumbCenter = Offset(trackActive, trackCenter);
_thumbPainter.paint(canvas, Rect.fromCircle(center: thumbCenter, radius: CupertinoThumbPainter.radius));
const CupertinoThumbPainter().paint(canvas, Rect.fromCircle(center: thumbCenter, radius: CupertinoThumbPainter.radius));
}
@override
......
......@@ -96,8 +96,8 @@ class CupertinoSwitch extends StatefulWidget {
/// The color to use when this switch is on.
///
/// Defaults to [CupertinoColors.activeGreen] when null and ignores the
/// [CupertinoTheme] in accordance to native iOS behavior.
/// Defaults to [CupertinoSystemColorsData.systemGreen] when null and ignores
/// the [CupertinoTheme] in accordance to native iOS behavior.
final Color activeColor;
/// {@template flutter.cupertino.switch.dragStartBehavior}
......@@ -140,7 +140,10 @@ class _CupertinoSwitchState extends State<CupertinoSwitch> with TickerProviderSt
opacity: widget.onChanged == null ? _kCupertinoSwitchDisabledOpacity : 1.0,
child: _CupertinoSwitchRenderObjectWidget(
value: widget.value,
activeColor: widget.activeColor ?? CupertinoColors.activeGreen,
activeColor: CupertinoDynamicColor.resolve(
widget.activeColor ?? CupertinoSystemColors.of(context).systemGreen,
context,
),
onChanged: widget.onChanged,
vsync: this,
dragStartBehavior: widget.dragStartBehavior,
......@@ -170,6 +173,7 @@ class _CupertinoSwitchRenderObjectWidget extends LeafRenderObjectWidget {
return _RenderCupertinoSwitch(
value: value,
activeColor: activeColor,
trackColor: CupertinoDynamicColor.resolve(CupertinoSystemColors.of(context).secondarySystemFill, context),
onChanged: onChanged,
textDirection: Directionality.of(context),
vsync: vsync,
......@@ -182,6 +186,7 @@ class _CupertinoSwitchRenderObjectWidget extends LeafRenderObjectWidget {
renderObject
..value = value
..activeColor = activeColor
..trackColor = CupertinoDynamicColor.resolve(CupertinoSystemColors.of(context).secondarySystemFill, context)
..onChanged = onChanged
..textDirection = Directionality.of(context)
..vsync = vsync
......@@ -200,7 +205,6 @@ const double _kSwitchHeight = 39.0;
// Opacity of a disabled switch, as eye-balled from iOS Simulator on Mac.
const double _kCupertinoSwitchDisabledOpacity = 0.5;
const Color _kTrackColor = CupertinoColors.lightBackgroundGray;
const Duration _kReactionDuration = Duration(milliseconds: 300);
const Duration _kToggleDuration = Duration(milliseconds: 200);
......@@ -208,6 +212,7 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox {
_RenderCupertinoSwitch({
@required bool value,
@required Color activeColor,
@required Color trackColor,
ValueChanged<bool> onChanged,
@required TextDirection textDirection,
@required TickerProvider vsync,
......@@ -217,6 +222,7 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox {
assert(vsync != null),
_value = value,
_activeColor = activeColor,
_trackColor = trackColor,
_onChanged = onChanged,
_textDirection = textDirection,
_vsync = vsync,
......@@ -295,6 +301,16 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox {
markNeedsPaint();
}
Color get trackColor => _trackColor;
Color _trackColor;
set trackColor(Color value) {
assert(value != null);
if (value == _trackColor)
return;
_trackColor = value;
markNeedsPaint();
}
ValueChanged<bool> get onChanged => _onChanged;
ValueChanged<bool> _onChanged;
set onChanged(ValueChanged<bool> value) {
......@@ -458,8 +474,6 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox {
config.isToggled = _value;
}
final CupertinoThumbPainter _thumbPainter = CupertinoThumbPainter();
@override
void paint(PaintingContext context, Offset offset) {
final Canvas canvas = context.canvas;
......@@ -478,7 +492,7 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox {
}
final Paint paint = Paint()
..color = Color.lerp(_kTrackColor, activeColor, currentValue);
..color = Color.lerp(trackColor, activeColor, currentValue);
final Rect trackRect = Rect.fromLTWH(
offset.dx + (size.width - _kTrackWidth) / 2.0,
......@@ -509,7 +523,7 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox {
);
context.pushClipRRect(needsCompositing, Offset.zero, thumbBounds, trackRRect, (PaintingContext innerContext, Offset offset) {
_thumbPainter.paint(innerContext.canvas, thumbBounds);
const CupertinoThumbPainter.switchThumb().paint(innerContext.canvas, thumbBounds);
});
}
......
......@@ -6,27 +6,62 @@ import 'package:flutter/painting.dart';
import 'colors.dart';
/// Paints an iOS-style slider thumb.
const Color _kThumbBorderColor = Color(0x0A000000);
const List<BoxShadow> _kSwitchBoxShadows = <BoxShadow> [
BoxShadow(
color: Color(0x26000000),
offset: Offset(0, 3),
blurRadius: 8.0,
),
BoxShadow(
color: Color(0x0F000000),
offset: Offset(0, 3),
blurRadius: 1.0,
),
];
const List<BoxShadow> _kSliderBoxShadows = <BoxShadow> [
BoxShadow(
color: Color(0x26000000),
offset: Offset(0, 3),
blurRadius: 8.0,
),
BoxShadow(
color: Color(0x29000000),
offset: Offset(0, 1),
blurRadius: 1.0,
),
BoxShadow(
color: Color(0x1A000000),
offset: Offset(0, 3),
blurRadius: 1.0,
),
];
/// Paints an iOS-style slider thumb or switch thumb.
///
/// Used by [CupertinoSwitch] and [CupertinoSlider].
class CupertinoThumbPainter {
/// Creates an object that paints an iOS-style slider thumb.
CupertinoThumbPainter({
const CupertinoThumbPainter({
this.color = CupertinoColors.white,
this.shadowColor = const Color(0x2C000000),
}) : _shadowPaint = BoxShadow(
color: shadowColor,
blurRadius: 1.0,
).toPaint();
this.shadows = _kSliderBoxShadows,
}) : assert(shadows != null);
/// Creates an object that paints an iOS-style switch thumb.
const CupertinoThumbPainter.switchThumb({
Color color = CupertinoColors.white,
List<BoxShadow> shadows = _kSwitchBoxShadows,
}) : this(color: color, shadows: shadows);
/// The color of the interior of the thumb.
final Color color;
/// The color of the shadow case by the thumb.
final Color shadowColor;
/// The paint used to draw the shadow case by the thumb.
final Paint _shadowPaint;
/// The list of [BoxShadow] to paint below the thumb.
///
/// Must not be null.
final List<BoxShadow> shadows;
/// Half the default diameter of the thumb.
static const double radius = 14.0;
......@@ -44,8 +79,13 @@ class CupertinoThumbPainter {
Radius.circular(rect.shortestSide / 2.0),
);
canvas.drawRRect(rrect, _shadowPaint);
canvas.drawRRect(rrect.shift(const Offset(0.0, 3.0)), _shadowPaint);
for (BoxShadow shadow in shadows)
canvas.drawRRect(rrect.shift(shadow.offset), shadow.toPaint());
canvas.drawRRect(
rrect.inflate(0.5),
Paint()..color = _kThumbBorderColor,
);
canvas.drawRRect(rrect, Paint()..color = color);
}
}
......@@ -44,11 +44,11 @@ const double _kMinInteractiveSize = 48.0;
class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
/// Creates a scrollbar with customizations given by construction arguments.
ScrollbarPainter({
@required this.color,
@required this.textDirection,
@required Color color,
@required TextDirection textDirection,
@required this.thickness,
@required this.fadeoutOpacityAnimation,
this.padding = EdgeInsets.zero,
EdgeInsets padding = EdgeInsets.zero,
this.mainAxisMargin = 0.0,
this.crossAxisMargin = 0.0,
this.radius,
......@@ -66,16 +66,37 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
assert(minOverscrollLength == null || minOverscrollLength >= 0),
assert(padding != null),
assert(padding.isNonNegative),
_color = color,
_textDirection = textDirection,
_padding = padding,
minOverscrollLength = minOverscrollLength ?? minLength {
fadeoutOpacityAnimation.addListener(notifyListeners);
}
/// [Color] of the thumb. Mustn't be null.
final Color color;
Color get color => _color;
Color _color;
set color(Color value) {
assert(value != null);
if (color == value)
return;
_color = value;
notifyListeners();
}
/// [TextDirection] of the [BuildContext] which dictates the side of the
/// screen the scrollbar appears in (the trailing side). Mustn't be null.
final TextDirection textDirection;
TextDirection get textDirection => _textDirection;
TextDirection _textDirection;
set textDirection(TextDirection value) {
assert(value != null);
if (textDirection == value)
return;
_textDirection = value;
notifyListeners();
}
/// Thickness of the scrollbar in its cross-axis in logical pixels. Mustn't be null.
double thickness;
......@@ -110,7 +131,17 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
///
/// Defaults to [EdgeInsets.zero]. Must not be null and offsets from all four
/// directions must be greater than or equal to zero.
final EdgeInsets padding;
EdgeInsets get padding => _padding;
EdgeInsets _padding;
set padding(EdgeInsets value) {
assert(value != null);
if (padding == value)
return;
_padding = value;
notifyListeners();
}
/// The preferred smallest size the scrollbar can shrink to when the total
/// scrollable extent is large, the current visible viewport is small, and the
......@@ -162,8 +193,8 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
}
Paint get _paint {
return Paint()..color =
color.withOpacity(color.opacity * fadeoutOpacityAnimation.value);
return Paint()
..color = color.withOpacity(color.opacity * fadeoutOpacityAnimation.value);
}
void _paintThumbCrossAxis(Canvas canvas, Size size, double thumbOffset, double thumbExtent, AxisDirection direction) {
......
......@@ -7,7 +7,7 @@ import 'package:flutter_test/flutter_test.dart';
import '../rendering/mock_canvas.dart';
const Color _kScrollbarColor = Color(0x99777777);
const Color _kScrollbarColor = Color(0x59000000);
// The `y` offset has to be larger than `ScrollDragController._bigThresholdBreakDistance`
// to prevent [motionStartDistanceThreshold] from affecting the actual drag distance.
......@@ -42,9 +42,9 @@ void main() {
color: _kScrollbarColor,
rrect: RRect.fromRectAndRadius(
const Rect.fromLTWH(
800.0 - 3 - 2.5, // Screen width - margin - thickness.
800.0 - 3 - 3, // Screen width - margin - thickness.
3.0, // Initial position is the top margin.
2.5, // Thickness.
3, // Thickness.
// Fraction in viewport * scrollbar height - top, bottom margin.
600.0 / 4000.0 * (600.0 - 2 * 3),
),
......@@ -86,9 +86,9 @@ void main() {
color: _kScrollbarColor,
rrect: RRect.fromRectAndRadius(
const Rect.fromLTWH(
800.0 - 3 - 2.5, // Screen width - margin - thickness.
800.0 - 3 - 3, // Screen width - margin - thickness.
44 + 20 + 3.0, // nav bar height + top margin
2.5, // Thickness.
3, // Thickness.
// Fraction visible * (viewport size - padding - margin)
// where Fraction visible = (viewport size - padding) / content size
(600.0 - 34 - 44 - 20) / 4000.0 * (600.0 - 2 * 3 - 34 - 44 - 20),
......
......@@ -8,6 +8,11 @@ import 'package:flutter_test/flutter_test.dart';
import '../rendering/mock_canvas.dart';
const CupertinoDynamicColor _kScrollbarColor = CupertinoDynamicColor.withBrightness(
color: Color(0x59000000),
darkColor:Color(0x80FFFFFF),
);
void main() {
const Duration _kScrollbarTimeToFade = Duration(milliseconds: 1200);
const Duration _kScrollbarFadeDuration = Duration(milliseconds: 250);
......@@ -31,14 +36,14 @@ void main() {
// Scrollbar fully showing
await tester.pump(const Duration(milliseconds: 500));
expect(find.byType(CupertinoScrollbar), paints..rrect(
color: const Color(0x99777777),
color: _kScrollbarColor.color,
));
await tester.pump(const Duration(seconds: 3));
await tester.pump(const Duration(seconds: 3));
// Still there.
expect(find.byType(CupertinoScrollbar), paints..rrect(
color: const Color(0x99777777),
color: _kScrollbarColor.color,
));
await gesture.up();
......@@ -47,7 +52,44 @@ void main() {
// Opacity going down now.
expect(find.byType(CupertinoScrollbar), paints..rrect(
color: const Color(0x77777777),
color: _kScrollbarColor.color.withAlpha(69),
));
});
testWidgets('Scrollbar dark mode', (WidgetTester tester) async {
Brightness brightness = Brightness.light;
StateSetter setState;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setter) {
setState = setter;
return MediaQuery(
data: MediaQueryData(platformBrightness: brightness),
child: const CupertinoScrollbar(
child: SingleChildScrollView(child: SizedBox(width: 4000.0, height: 4000.0)),
),
);
},
),
),
);
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(SingleChildScrollView)));
await gesture.moveBy(const Offset(0.0, 10.0));
await tester.pump();
// Scrollbar fully showing
await tester.pumpAndSettle();
expect(find.byType(CupertinoScrollbar), paints..rrect(
color: _kScrollbarColor.color,
));
setState(() { brightness = Brightness.dark; });
await tester.pump();
expect(find.byType(CupertinoScrollbar), paints..rrect(
color: _kScrollbarColor.darkColor,
));
});
......@@ -81,7 +123,7 @@ void main() {
// Scrollbar thumb is fully showing and scroll offset has moved by
// scrollAmount.
expect(find.byType(CupertinoScrollbar), paints..rrect(
color: const Color(0x99777777),
color: _kScrollbarColor.color,
));
expect(scrollController.offset, scrollAmount);
await scrollGesture.up();
......@@ -111,7 +153,7 @@ void main() {
expect(scrollController.offset, greaterThan(scrollAmount * 2));
// The scrollbar thumb is still fully visible.
expect(find.byType(CupertinoScrollbar), paints..rrect(
color: const Color(0x99777777),
color: _kScrollbarColor.color,
));
// Let the thumb fade out so all timers have resolved.
......@@ -149,7 +191,7 @@ void main() {
// Scrollbar thumb is fully showing and scroll offset has moved by
// scrollAmount.
expect(find.byType(CupertinoScrollbar), paints..rrect(
color: const Color(0x99777777),
color: _kScrollbarColor.color,
));
expect(scrollController.offset, scrollAmount);
await scrollGesture.up();
......@@ -182,7 +224,7 @@ void main() {
expect(scrollController.offset, greaterThan(scrollAmount * 2));
// The scrollbar thumb is still fully visible.
expect(find.byType(CupertinoScrollbar), paints..rrect(
color: const Color(0x99777777),
color: _kScrollbarColor.color,
));
// Let the thumb fade out so all timers have resolved.
......
......@@ -532,8 +532,8 @@ void main() {
value = newValue;
});
},
)
)
),
),
);
},
),
......@@ -544,7 +544,7 @@ void main() {
find.byKey(switchKey),
matchesGoldenFile(
'switch.tap.off.png',
version: 0,
version: 1,
),
);
......@@ -558,7 +558,7 @@ void main() {
find.byKey(switchKey),
matchesGoldenFile(
'switch.tap.turningOn.png',
version: 0,
version: 1,
),
);
......@@ -567,9 +567,59 @@ void main() {
find.byKey(switchKey),
matchesGoldenFile(
'switch.tap.on.png',
version: 0,
version: 1,
),
);
});
testWidgets('Switch renders correctly in dark mode', (WidgetTester tester) async {
final Key switchKey = UniqueKey();
bool value = false;
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(platformBrightness: Brightness.dark),
child: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Center(
child: RepaintBoundary(
child: CupertinoSwitch(
key: switchKey,
value: value,
dragStartBehavior: DragStartBehavior.down,
onChanged: (bool newValue) {
setState(() {
value = newValue;
});
},
),
),
);
},
),
),
),
);
await expectLater(
find.byKey(switchKey),
matchesGoldenFile(
'switch.tap.off.dark.png',
version: 0,
),
);
await tester.tap(find.byKey(switchKey));
expect(value, isTrue);
await tester.pumpAndSettle();
await expectLater(
find.byKey(switchKey),
matchesGoldenFile(
'switch.tap.on.dark.png',
version: 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