[Material] Adaptive Slider constructor (#30572)

Adds an adaptive constructor for the Material Slider. An adaptive widget is one that renders itself as Material on Android, and Cupertino on iOS. This work is based off of a similar feature on Switches: bbb080b#diff-fe2bb980c6207699cbf45538fe927afa.

The motivation for this change is that we should provide adaptive constructors for as many widgets as necessary in the Material library. In Material, it is suggested that the slider is an iOS-style slider.
This commit is contained in:
Anthony 2019-04-24 16:26:22 -04:00 committed by GitHub
parent 1da7f1b9d5
commit 0572f158fb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 136 additions and 6 deletions

View file

@ -146,7 +146,7 @@ class _SliderDemoState extends State<SliderDemo> {
Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Slider(
Slider.adaptive(
value: _value,
min: 0.0,
max: 100.0,
@ -165,7 +165,7 @@ class _SliderDemoState extends State<SliderDemo> {
Row(
children: <Widget>[
Expanded(
child: Slider(
child: Slider.adaptive(
value: _value,
min: 0.0,
max: 100.0,
@ -205,14 +205,14 @@ class _SliderDemoState extends State<SliderDemo> {
Column(
mainAxisSize: MainAxisSize.min,
children: const <Widget>[
Slider(value: 0.25, onChanged: null),
Slider.adaptive(value: 0.25, onChanged: null),
Text('Disabled'),
],
),
Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Slider(
Slider.adaptive(
value: _discreteValue,
min: 0.0,
max: 200.0,

View file

@ -6,6 +6,7 @@ import 'dart:async';
import 'dart:math' as math;
import 'dart:math';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
@ -30,6 +31,8 @@ import 'theme.dart';
/// * [Slider.semanticFormatterCallback], which shows an example use case.
typedef SemanticFormatterCallback = String Function(double value);
enum _SliderType { material, adaptive }
/// A Material Design slider.
///
/// Used to select from a range of values.
@ -89,7 +92,7 @@ typedef SemanticFormatterCallback = String Function(double value);
/// * <https://material.io/design/components/sliders.html>
/// * [MediaQuery], from which the text scale factor is obtained.
class Slider extends StatefulWidget {
/// Creates a material design slider.
/// Creates a Material Design slider.
///
/// The slider itself does not maintain any state. Instead, when the state of
/// the slider changes, the widget calls the [onChanged] callback. Most
@ -121,7 +124,37 @@ class Slider extends StatefulWidget {
this.activeColor,
this.inactiveColor,
this.semanticFormatterCallback,
}) : assert(value != null),
}) : _sliderType = _SliderType.material,
assert(value != null),
assert(min != null),
assert(max != null),
assert(min <= max),
assert(value >= min && value <= max),
assert(divisions == null || divisions > 0),
super(key: key);
/// Creates a [CupertinoSlider] if the target platform is iOS, creates a
/// Material Design slider otherwise.
///
/// If a [CupertinoSlider] is created, the following parameters are
/// ignored: [label], [inactiveColor], [semanticFormatterCallback].
///
/// The target platform is based on the current [Theme]: [ThemeData.platform].
const Slider.adaptive({
Key key,
@required this.value,
@required this.onChanged,
this.onChangeStart,
this.onChangeEnd,
this.min = 0.0,
this.max = 1.0,
this.divisions,
this.label,
this.activeColor,
this.inactiveColor,
this.semanticFormatterCallback,
}) : _sliderType = _SliderType.adaptive,
assert(value != null),
assert(min != null),
assert(max != null),
assert(min <= max),
@ -273,6 +306,8 @@ class Slider extends StatefulWidget {
///
/// If null, then the value indicator will not be displayed.
///
/// Ignored if this slider is created with [Slider.adaptive].
///
/// See also:
///
/// * [SliderComponentShape] for how to create a custom value indicator
@ -300,6 +335,8 @@ class Slider extends StatefulWidget {
///
/// Using a [SliderTheme] gives much more fine-grained control over the
/// appearance of various components of the slider.
///
/// Ignored if this slider is created with [Slider.adaptive].
final Color inactiveColor;
/// The callback used to create a semantic value from a slider value.
@ -331,8 +368,12 @@ class Slider extends StatefulWidget {
/// )
/// ```
/// {@end-tool}
///
/// Ignored if this slider is created with [Slider.adaptive]
final SemanticFormatterCallback semanticFormatterCallback;
final _SliderType _sliderType ;
@override
_SliderState createState() => _SliderState();
@ -441,6 +482,27 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin {
assert(debugCheckHasMaterial(context));
assert(debugCheckHasMediaQuery(context));
switch (widget._sliderType) {
case _SliderType.material:
return _buildMaterialSlider(context);
case _SliderType.adaptive: {
final ThemeData theme = Theme.of(context);
assert(theme.platform != null);
switch (theme.platform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
return _buildMaterialSlider(context);
case TargetPlatform.iOS:
return _buildCupertinoSlider(context);
}
}
}
assert(false);
return null;
}
Widget _buildMaterialSlider(BuildContext context) {
final ThemeData theme = Theme.of(context);
SliderThemeData sliderTheme = SliderTheme.of(context);
@ -488,6 +550,25 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin {
semanticFormatterCallback: widget.semanticFormatterCallback,
);
}
Widget _buildCupertinoSlider(BuildContext context) {
// The render box of a slider has a fixed height but takes up the available
// width. Wrapping the [CupertinoSlider] in this manner will help maintain
// the same size.
return SizedBox(
width: double.infinity,
child: CupertinoSlider(
value: widget.value,
onChanged: widget.onChanged,
onChangeStart: widget.onChangeStart,
onChangeEnd: widget.onChangeEnd,
min: widget.min,
max: widget.max,
divisions: widget.divisions,
activeColor: widget.activeColor,
),
);
}
}
class _SliderRenderObjectWidget extends LeafRenderObjectWidget {

View file

@ -4,6 +4,7 @@
import 'dart:ui';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
@ -1436,4 +1437,52 @@ void main() {
expect(await tester.pumpAndSettle(const Duration(milliseconds: 100)), equals(1));
await gesture.up();
});
testWidgets('Slider.adaptive', (WidgetTester tester) async {
double value = 0.5;
Widget buildFrame(TargetPlatform platform) {
return MaterialApp(
theme: ThemeData(platform: platform),
home: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Material(
child: Center(
child: Slider.adaptive(
value: value,
onChanged: (double newValue) {
setState(() {
value = newValue;
});
},
),
),
);
},
),
);
}
await tester.pumpWidget(buildFrame(TargetPlatform.iOS));
expect(find.byType(Slider), findsOneWidget);
expect(find.byType(CupertinoSlider), findsOneWidget);
expect(value, 0.5);
TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(CupertinoSlider)));
// Drag to the right end of the track.
await gesture.moveBy(const Offset(600.0, 0.0));
expect(value, 1.0);
value = 0.5;
await tester.pumpWidget(buildFrame(TargetPlatform.android));
await tester.pumpAndSettle(); // Finish the theme change animation.
expect(find.byType(Slider), findsOneWidget);
expect(find.byType(CupertinoSlider), findsNothing);
expect(value, 0.5);
gesture = await tester.startGesture(tester.getCenter(find.byType(Slider)));
// Drag to the right end of the track.
await gesture.moveBy(const Offset(600.0, 0.0));
expect(value, 1.0);
});
}