Slider: add themeable mouse cursor v2 (#96623)

This commit is contained in:
Hans Muller 2022-01-13 17:07:02 -08:00 committed by GitHub
parent e78f135fd5
commit c24b2c3c6e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 139 additions and 16 deletions

View file

@ -373,17 +373,26 @@ class Slider extends StatefulWidget {
/// (like the native default iOS slider).
final Color? thumbColor;
/// {@template flutter.material.slider.mouseCursor}
/// The cursor for a mouse pointer when it enters or is hovering over the
/// widget.
///
/// If [mouseCursor] is a [MaterialStateProperty<MouseCursor>],
/// [MaterialStateProperty.resolve] is used for the following [MaterialState]s:
///
/// * [MaterialState.dragged].
/// * [MaterialState.hovered].
/// * [MaterialState.focused].
/// * [MaterialState.disabled].
/// {@endtemplate}
///
/// If this property is null, [MaterialStateMouseCursor.clickable] will be used.
/// If null, then the value of [SliderThemeData.mouseCursor] is used. If that
/// is also null, then [MaterialStateMouseCursor.clickable] is used.
///
/// See also:
///
/// * [MaterialStateMouseCursor], which can be used to create a [MouseCursor]
/// that is also a [MaterialStateProperty<MouseCursor>].
final MouseCursor? mouseCursor;
/// The callback used to create a semantic value from a slider value.
@ -481,6 +490,8 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin {
// Value Indicator Animation that appears on the Overlay.
PaintValueIndicator? paintValueIndicator;
bool _dragging = false;
FocusNode? _focusNode;
FocusNode get focusNode => widget.focusNode ?? _focusNode!;
@ -540,13 +551,13 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin {
}
void _handleDragStart(double value) {
assert(widget.onChangeStart != null);
widget.onChangeStart!(_lerp(value));
_dragging = true;
widget.onChangeStart?.call(_lerp(value));
}
void _handleDragEnd(double value) {
assert(widget.onChangeEnd != null);
widget.onChangeEnd!(_lerp(value));
_dragging = false;
widget.onChangeEnd?.call(_lerp(value));
}
void _actionHandler(_AdjustSliderIntent intent) {
@ -692,14 +703,15 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin {
color: theme.colorScheme.onPrimary,
),
);
final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor>(
widget.mouseCursor ?? MaterialStateMouseCursor.clickable,
<MaterialState>{
if (!_enabled) MaterialState.disabled,
if (_hovering) MaterialState.hovered,
if (_focused) MaterialState.focused,
},
);
final Set<MaterialState> states = <MaterialState>{
if (!_enabled) MaterialState.disabled,
if (_hovering) MaterialState.hovered,
if (_focused) MaterialState.focused,
if (_dragging) MaterialState.dragged,
};
final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor?>(widget.mouseCursor, states)
?? sliderTheme.mouseCursor?.resolve(states)
?? MaterialStateMouseCursor.clickable.resolve(states);
// This size is used as the max bounds for the painting of the value
// indicators It must be kept in sync with the function with the same name
@ -748,8 +760,8 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin {
textScaleFactor: MediaQuery.of(context).textScaleFactor,
screenSize: _screenSize(),
onChanged: (widget.onChanged != null) && (widget.max > widget.min) ? _handleChanged : null,
onChangeStart: widget.onChangeStart != null ? _handleDragStart : null,
onChangeEnd: widget.onChangeEnd != null ? _handleDragEnd : null,
onChangeStart: _handleDragStart,
onChangeEnd: _handleDragEnd,
state: this,
semanticFormatterCallback: widget.semanticFormatterCallback,
hasFocus: _focused,

View file

@ -10,6 +10,7 @@ import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'colors.dart';
import 'material_state.dart';
import 'theme.dart';
/// Applies a slider theme to descendant [Slider] widgets.
@ -290,6 +291,7 @@ class SliderThemeData with Diagnosticable {
this.valueIndicatorTextStyle,
this.minThumbSeparation,
this.thumbSelector,
this.mouseCursor,
});
/// Generates a SliderThemeData from three main colors.
@ -561,6 +563,11 @@ class SliderThemeData with Diagnosticable {
/// Override this for custom thumb selection.
final RangeThumbSelector? thumbSelector;
/// {@macro flutter.material.slider.mouseCursor}
///
/// If specified, overrides the default value of [Slider.mouseCursor].
final MaterialStateProperty<MouseCursor?>? mouseCursor;
/// Creates a copy of this object but with the given fields replaced with the
/// new values.
SliderThemeData copyWith({
@ -591,6 +598,7 @@ class SliderThemeData with Diagnosticable {
TextStyle? valueIndicatorTextStyle,
double? minThumbSeparation,
RangeThumbSelector? thumbSelector,
MaterialStateProperty<MouseCursor?>? mouseCursor,
}) {
return SliderThemeData(
trackHeight: trackHeight ?? this.trackHeight,
@ -620,6 +628,7 @@ class SliderThemeData with Diagnosticable {
valueIndicatorTextStyle: valueIndicatorTextStyle ?? this.valueIndicatorTextStyle,
minThumbSeparation: minThumbSeparation ?? this.minThumbSeparation,
thumbSelector: thumbSelector ?? this.thumbSelector,
mouseCursor: mouseCursor ?? this.mouseCursor,
);
}
@ -660,6 +669,7 @@ class SliderThemeData with Diagnosticable {
valueIndicatorTextStyle: TextStyle.lerp(a.valueIndicatorTextStyle, b.valueIndicatorTextStyle, t),
minThumbSeparation: lerpDouble(a.minThumbSeparation, b.minThumbSeparation, t),
thumbSelector: t < 0.5 ? a.thumbSelector : b.thumbSelector,
mouseCursor: t < 0.5 ? a.mouseCursor : b.mouseCursor,
);
}
@ -693,6 +703,7 @@ class SliderThemeData with Diagnosticable {
valueIndicatorTextStyle,
minThumbSeparation,
thumbSelector,
mouseCursor,
]);
}
@ -731,7 +742,8 @@ class SliderThemeData with Diagnosticable {
&& other.showValueIndicator == showValueIndicator
&& other.valueIndicatorTextStyle == valueIndicatorTextStyle
&& other.minThumbSeparation == minThumbSeparation
&& other.thumbSelector == thumbSelector;
&& other.thumbSelector == thumbSelector
&& other.mouseCursor == mouseCursor;
}
@override
@ -765,6 +777,7 @@ class SliderThemeData with Diagnosticable {
properties.add(DiagnosticsProperty<TextStyle>('valueIndicatorTextStyle', valueIndicatorTextStyle, defaultValue: defaultData.valueIndicatorTextStyle));
properties.add(DoubleProperty('minThumbSeparation', minThumbSeparation, defaultValue: defaultData.minThumbSeparation));
properties.add(DiagnosticsProperty<RangeThumbSelector>('thumbSelector', thumbSelector, defaultValue: defaultData.thumbSelector));
properties.add(DiagnosticsProperty<MaterialStateProperty<MouseCursor?>>('mouseCursor', mouseCursor, defaultValue: defaultData.mouseCursor));
}
}

View file

@ -70,6 +70,35 @@ class TallSliderTickMarkShape extends SliderTickMarkShape {
}
}
class _StateDependentMouseCursor extends MaterialStateMouseCursor {
const _StateDependentMouseCursor({
this.disabled = SystemMouseCursors.none,
this.dragged = SystemMouseCursors.none,
this.hovered = SystemMouseCursors.none,
});
final MouseCursor disabled;
final MouseCursor hovered;
final MouseCursor dragged;
@override
MouseCursor resolve(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return disabled;
}
if (states.contains(MaterialState.dragged)) {
return dragged;
}
if (states.contains(MaterialState.hovered)) {
return hovered;
}
return SystemMouseCursors.none;
}
@override
String get debugDescription => '_StateDependentMouseCursor';
}
void main() {
testWidgets('Slider can move when tapped (LTR)', (WidgetTester tester) async {
final Key sliderKey = UniqueKey();
@ -2521,6 +2550,57 @@ void main() {
expect(RendererBinding.instance!.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click);
});
testWidgets('Slider MaterialStateMouseCursor resolves correctly', (WidgetTester tester) async {
const MouseCursor disabledCursor = SystemMouseCursors.basic;
const MouseCursor hoveredCursor = SystemMouseCursors.grab;
const MouseCursor draggedCursor = SystemMouseCursors.move;
Widget buildFrame({ required bool enabled }) {
return MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: Material(
child: Center(
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: Slider(
mouseCursor: const _StateDependentMouseCursor(
disabled: disabledCursor,
hovered: hoveredCursor,
dragged: draggedCursor,
),
value: 0.5,
onChanged: enabled ? (double newValue) { } : null,
),
),
),
),
),
);
}
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
await gesture.addPointer(location: Offset.zero);
addTearDown(gesture.removePointer);
await tester.pumpWidget(buildFrame(enabled: false));
expect(RendererBinding.instance!.mouseTracker.debugDeviceActiveCursor(1), disabledCursor);
await tester.pumpWidget(buildFrame(enabled: true));
expect(RendererBinding.instance!.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.none);
await gesture.moveTo(tester.getCenter(find.byType(Slider))); // start hover
await tester.pumpAndSettle();
expect(RendererBinding.instance!.mouseTracker.debugDeviceActiveCursor(1), hoveredCursor);
await tester.timedDrag(
find.byType(Slider),
const Offset(20.0, 0.0),
const Duration(milliseconds: 100),
);
expect(RendererBinding.instance!.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.move);
});
testWidgets('Slider implements debugFillProperties', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();

View file

@ -4,6 +4,7 @@
import 'dart:ui' show window;
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
@ -57,6 +58,7 @@ void main() {
rangeValueIndicatorShape: PaddleRangeSliderValueIndicatorShape(),
showValueIndicator: ShowValueIndicator.always,
valueIndicatorTextStyle: TextStyle(color: Colors.black),
mouseCursor: MaterialStateMouseCursor.clickable,
).debugFillProperties(builder);
final List<String> description = builder.properties
@ -90,6 +92,7 @@ void main() {
"rangeValueIndicatorShape: Instance of 'PaddleRangeSliderValueIndicatorShape'",
'showValueIndicator: always',
'valueIndicatorTextStyle: TextStyle(inherit: true, color: Color(0xff000000))',
'mouseCursor: MaterialStateMouseCursor(clickable)'
]);
});
@ -1242,6 +1245,21 @@ void main() {
);
});
testWidgets('The mouse cursor is themeable', (WidgetTester tester) async {
await tester.pumpWidget(_buildApp(
ThemeData().sliderTheme.copyWith(
mouseCursor: MaterialStateProperty.all(SystemMouseCursors.text),
)
));
await tester.pumpAndSettle();
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
addTearDown(gesture.removePointer);
await gesture.moveTo(tester.getCenter(find.byType(Slider)));
await tester.pumpAndSettle();
expect(RendererBinding.instance!.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
});
}
class RoundedRectSliderTrackShapeWithCustomAdditionalActiveTrackHeight extends RoundedRectSliderTrackShape {