mirror of
https://github.com/flutter/flutter
synced 2024-10-13 03:32:55 +00:00
[Material] Update slider gallery demo, including range slider (#34597)
Update the flutter_gallery Slider demo by aligning the text field in the first slider, adding range slider examples (including custom range thumbs), and separating single slider and range slider examples with tabs.
This commit is contained in:
parent
a276ab3d6d
commit
04bd87794d
|
@ -15,19 +15,95 @@ class SliderDemo extends StatefulWidget {
|
|||
_SliderDemoState createState() => _SliderDemoState();
|
||||
}
|
||||
|
||||
Path _triangle(double size, Offset thumbCenter, {bool invert = false}) {
|
||||
Path _downTriangle(double size, Offset thumbCenter, { bool invert = false }) {
|
||||
final Path thumbPath = Path();
|
||||
final double height = math.sqrt(3.0) / 2.0;
|
||||
final double halfSide = size / 2.0;
|
||||
final double centerHeight = size * height / 3.0;
|
||||
final double halfSize = size / 2.0;
|
||||
final double sign = invert ? -1.0 : 1.0;
|
||||
thumbPath.moveTo(thumbCenter.dx - halfSide, thumbCenter.dy + sign * centerHeight);
|
||||
thumbPath.moveTo(thumbCenter.dx - halfSize, thumbCenter.dy + sign * centerHeight);
|
||||
thumbPath.lineTo(thumbCenter.dx, thumbCenter.dy - 2.0 * sign * centerHeight);
|
||||
thumbPath.lineTo(thumbCenter.dx + halfSide, thumbCenter.dy + sign * centerHeight);
|
||||
thumbPath.lineTo(thumbCenter.dx + halfSize, thumbCenter.dy + sign * centerHeight);
|
||||
thumbPath.close();
|
||||
return thumbPath;
|
||||
}
|
||||
|
||||
Path _rightTriangle(double size, Offset thumbCenter, { bool invert = false }) {
|
||||
final Path thumbPath = Path();
|
||||
final double halfSize = size / 2.0;
|
||||
final double sign = invert ? -1.0 : 1.0;
|
||||
thumbPath.moveTo(thumbCenter.dx + halfSize * sign, thumbCenter.dy);
|
||||
thumbPath.lineTo(thumbCenter.dx - halfSize * sign, thumbCenter.dy - size);
|
||||
thumbPath.lineTo(thumbCenter.dx - halfSize * sign, thumbCenter.dy + size);
|
||||
thumbPath.close();
|
||||
return thumbPath;
|
||||
}
|
||||
|
||||
Path _upTriangle(double size, Offset thumbCenter) => _downTriangle(size, thumbCenter, invert: true);
|
||||
|
||||
Path _leftTriangle(double size, Offset thumbCenter) => _rightTriangle(size, thumbCenter, invert: true);
|
||||
|
||||
class _CustomRangeThumbShape extends RangeSliderThumbShape {
|
||||
static const double _thumbSize = 4.0;
|
||||
static const double _disabledThumbSize = 3.0;
|
||||
|
||||
@override
|
||||
Size getPreferredSize(bool isEnabled, bool isDiscrete) {
|
||||
return isEnabled ? const Size.fromRadius(_thumbSize) : const Size.fromRadius(_disabledThumbSize);
|
||||
}
|
||||
|
||||
static final Animatable<double> sizeTween = Tween<double>(
|
||||
begin: _disabledThumbSize,
|
||||
end: _thumbSize,
|
||||
);
|
||||
|
||||
@override
|
||||
void paint(
|
||||
PaintingContext context,
|
||||
Offset center, {
|
||||
@required Animation<double> activationAnimation,
|
||||
@required Animation<double> enableAnimation,
|
||||
bool isDiscrete = false,
|
||||
bool isEnabled = false,
|
||||
bool isOnTop,
|
||||
@required SliderThemeData sliderTheme,
|
||||
TextDirection textDirection,
|
||||
Thumb thumb,
|
||||
}) {
|
||||
final Canvas canvas = context.canvas;
|
||||
final ColorTween colorTween = ColorTween(
|
||||
begin: sliderTheme.disabledThumbColor,
|
||||
end: sliderTheme.thumbColor,
|
||||
);
|
||||
|
||||
final double size = _thumbSize * sizeTween.evaluate(enableAnimation);
|
||||
Path thumbPath;
|
||||
switch (textDirection) {
|
||||
case TextDirection.rtl:
|
||||
switch (thumb) {
|
||||
case Thumb.start:
|
||||
thumbPath = _rightTriangle(size, center);
|
||||
break;
|
||||
case Thumb.end:
|
||||
thumbPath = _leftTriangle(size, center);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case TextDirection.ltr:
|
||||
switch (thumb) {
|
||||
case Thumb.start:
|
||||
thumbPath = _leftTriangle(size, center);
|
||||
break;
|
||||
case Thumb.end:
|
||||
thumbPath = _rightTriangle(size, center);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
canvas.drawPath(thumbPath, Paint()..color = colorTween.evaluate(enableAnimation));
|
||||
}
|
||||
}
|
||||
|
||||
class _CustomThumbShape extends SliderComponentShape {
|
||||
static const double _thumbSize = 4.0;
|
||||
static const double _disabledThumbSize = 3.0;
|
||||
|
@ -61,7 +137,7 @@ class _CustomThumbShape extends SliderComponentShape {
|
|||
end: sliderTheme.thumbColor,
|
||||
);
|
||||
final double size = _thumbSize * sizeTween.evaluate(enableAnimation);
|
||||
final Path thumbPath = _triangle(size, thumbCenter);
|
||||
final Path thumbPath = _downTriangle(size, thumbCenter);
|
||||
canvas.drawPath(thumbPath, Paint()..color = colorTween.evaluate(enableAnimation));
|
||||
}
|
||||
}
|
||||
|
@ -105,11 +181,7 @@ class _CustomValueIndicatorShape extends SliderComponentShape {
|
|||
);
|
||||
final double size = _indicatorSize * sizeTween.evaluate(enableAnimation);
|
||||
final Offset slideUpOffset = Offset(0.0, -slideUpTween.evaluate(activationAnimation));
|
||||
final Path thumbPath = _triangle(
|
||||
size,
|
||||
thumbCenter + slideUpOffset,
|
||||
invert: true,
|
||||
);
|
||||
final Path thumbPath = _upTriangle(size, thumbCenter + slideUpOffset);
|
||||
final Color paintColor = enableColor.evaluate(enableAnimation).withAlpha((255.0 * activationAnimation.value).round());
|
||||
canvas.drawPath(
|
||||
thumbPath,
|
||||
|
@ -127,142 +199,243 @@ class _CustomValueIndicatorShape extends SliderComponentShape {
|
|||
}
|
||||
|
||||
class _SliderDemoState extends State<SliderDemo> {
|
||||
double _value = 25.0;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final List<ComponentDemoTabData> demos = <ComponentDemoTabData>[
|
||||
ComponentDemoTabData(
|
||||
tabName: 'SINGLE',
|
||||
description: 'Sliders containing 1 thumb',
|
||||
demoWidget: _Sliders(),
|
||||
documentationUrl: 'https://docs.flutter.io/flutter/material/Slider-class.html',
|
||||
),
|
||||
ComponentDemoTabData(
|
||||
tabName: 'RANGE',
|
||||
description: 'Sliders containing 2 thumbs',
|
||||
demoWidget: _RangeSliders(),
|
||||
documentationUrl: 'https://docs.flutter.io/flutter/material/RangeSlider-class.html',
|
||||
),
|
||||
];
|
||||
|
||||
return TabbedComponentDemoScaffold(
|
||||
title: 'Sliders',
|
||||
demos: demos,
|
||||
isScrollable: false,
|
||||
showExampleCodeAction: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Sliders extends StatefulWidget {
|
||||
@override
|
||||
_SlidersState createState() => _SlidersState();
|
||||
}
|
||||
|
||||
class _SlidersState extends State<_Sliders> {
|
||||
double _continuousValue = 25.0;
|
||||
double _discreteValue = 20.0;
|
||||
double _discreteCustomValue = 25.0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Sliders'),
|
||||
actions: <Widget>[MaterialDemoDocumentationButton(SliderDemo.routeName)],
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 40.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: <Widget>[
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Slider.adaptive(
|
||||
value: _value,
|
||||
min: 0.0,
|
||||
max: 100.0,
|
||||
onChanged: (double value) {
|
||||
setState(() {
|
||||
_value = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
const Text('Continuous'),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Slider.adaptive(
|
||||
value: _value,
|
||||
min: 0.0,
|
||||
max: 100.0,
|
||||
onChanged: (double value) {
|
||||
setState(() {
|
||||
_value = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 40.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: <Widget>[
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Semantics(
|
||||
label: 'Editable numerical value',
|
||||
child: SizedBox(
|
||||
width: 64,
|
||||
height: 48,
|
||||
child: TextField(
|
||||
textAlign: TextAlign.center,
|
||||
onSubmitted: (String value) {
|
||||
final double newValue = double.tryParse(value);
|
||||
if (newValue != null && newValue != _continuousValue) {
|
||||
setState(() {
|
||||
_continuousValue = newValue.clamp(0, 100);
|
||||
});
|
||||
}
|
||||
},
|
||||
keyboardType: TextInputType.number,
|
||||
controller: TextEditingController(
|
||||
text: _continuousValue.toStringAsFixed(0),
|
||||
),
|
||||
Semantics(
|
||||
label: 'Editable numerical value',
|
||||
child: Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
child: TextField(
|
||||
onSubmitted: (String value) {
|
||||
final double newValue = double.tryParse(value);
|
||||
if (newValue != null && newValue != _value) {
|
||||
setState(() {
|
||||
_value = newValue.clamp(0, 100);
|
||||
});
|
||||
}
|
||||
},
|
||||
keyboardType: TextInputType.number,
|
||||
controller: TextEditingController(
|
||||
text: _value.toStringAsFixed(0),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Text('Continuous with Editable Numerical Value'),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: const <Widget>[
|
||||
Slider.adaptive(value: 0.25, onChanged: null),
|
||||
Text('Disabled'),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Slider.adaptive(
|
||||
value: _discreteValue,
|
||||
),
|
||||
Slider.adaptive(
|
||||
value: _continuousValue,
|
||||
min: 0.0,
|
||||
max: 100.0,
|
||||
onChanged: (double value) {
|
||||
setState(() {
|
||||
_continuousValue = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
const Text('Continuous with Editable Numerical Value'),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: const <Widget>[
|
||||
Slider.adaptive(value: 0.25, onChanged: null),
|
||||
Text('Disabled'),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Slider.adaptive(
|
||||
value: _discreteValue,
|
||||
min: 0.0,
|
||||
max: 200.0,
|
||||
divisions: 5,
|
||||
label: '${_discreteValue.round()}',
|
||||
onChanged: (double value) {
|
||||
setState(() {
|
||||
_discreteValue = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
const Text('Discrete'),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
SliderTheme(
|
||||
data: theme.sliderTheme.copyWith(
|
||||
activeTrackColor: Colors.deepPurple,
|
||||
inactiveTrackColor: theme.colorScheme.onSurface.withOpacity(0.5),
|
||||
activeTickMarkColor: theme.colorScheme.onSurface.withOpacity(0.7),
|
||||
inactiveTickMarkColor: theme.colorScheme.surface.withOpacity(0.7),
|
||||
overlayColor: theme.colorScheme.onSurface.withOpacity(0.12),
|
||||
thumbColor: Colors.deepPurple,
|
||||
valueIndicatorColor: Colors.deepPurpleAccent,
|
||||
thumbShape: _CustomThumbShape(),
|
||||
valueIndicatorShape: _CustomValueIndicatorShape(),
|
||||
valueIndicatorTextStyle: theme.accentTextTheme.body2.copyWith(color: theme.colorScheme.onSurface),
|
||||
),
|
||||
child: Slider(
|
||||
value: _discreteCustomValue,
|
||||
min: 0.0,
|
||||
max: 200.0,
|
||||
divisions: 5,
|
||||
label: '${_discreteValue.round()}',
|
||||
semanticFormatterCallback: (double value) => value.round().toString(),
|
||||
label: '${_discreteCustomValue.round()}',
|
||||
onChanged: (double value) {
|
||||
setState(() {
|
||||
_discreteValue = value;
|
||||
_discreteCustomValue = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
const Text('Discrete'),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
SliderTheme(
|
||||
data: theme.sliderTheme.copyWith(
|
||||
activeTrackColor: Colors.deepPurple,
|
||||
inactiveTrackColor: Colors.black26,
|
||||
activeTickMarkColor: Colors.white70,
|
||||
inactiveTickMarkColor: Colors.black,
|
||||
overlayColor: Colors.black12,
|
||||
thumbColor: Colors.deepPurple,
|
||||
valueIndicatorColor: Colors.deepPurpleAccent,
|
||||
thumbShape: _CustomThumbShape(),
|
||||
valueIndicatorShape: _CustomValueIndicatorShape(),
|
||||
valueIndicatorTextStyle: theme.accentTextTheme.body2.copyWith(color: Colors.black87),
|
||||
),
|
||||
child: Slider(
|
||||
value: _discreteValue,
|
||||
min: 0.0,
|
||||
max: 200.0,
|
||||
divisions: 5,
|
||||
semanticFormatterCallback: (double value) => value.round().toString(),
|
||||
label: '${_discreteValue.round()}',
|
||||
onChanged: (double value) {
|
||||
setState(() {
|
||||
_discreteValue = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
const Text('Discrete with Custom Theme'),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Text('Discrete with Custom Theme'),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RangeSliders extends StatefulWidget {
|
||||
@override
|
||||
_RangeSlidersState createState() => _RangeSlidersState();
|
||||
}
|
||||
|
||||
class _RangeSlidersState extends State<_RangeSliders> {
|
||||
RangeValues _continuousValues = const RangeValues(25.0, 75.0);
|
||||
RangeValues _discreteValues = const RangeValues(40.0, 120.0);
|
||||
RangeValues _discreteCustomValues = const RangeValues(40.0, 160.0);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 40.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: <Widget>[
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
RangeSlider(
|
||||
values: _continuousValues,
|
||||
min: 0.0,
|
||||
max: 100.0,
|
||||
onChanged: (RangeValues values) {
|
||||
setState(() {
|
||||
_continuousValues = values;
|
||||
});
|
||||
},
|
||||
),
|
||||
const Text('Continuous'),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
RangeSlider(values: const RangeValues(0.25, 0.75), onChanged: null),
|
||||
const Text('Disabled'),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
RangeSlider(
|
||||
values: _discreteValues,
|
||||
min: 0.0,
|
||||
max: 200.0,
|
||||
divisions: 5,
|
||||
labels: RangeLabels('${_discreteValues.start.round()}', '${_discreteValues.end.round()}'),
|
||||
onChanged: (RangeValues values) {
|
||||
setState(() {
|
||||
_discreteValues = values;
|
||||
});
|
||||
},
|
||||
),
|
||||
const Text('Discrete'),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
SliderTheme(
|
||||
data: SliderThemeData(
|
||||
activeTrackColor: Colors.deepPurple,
|
||||
inactiveTrackColor: Colors.black26,
|
||||
activeTickMarkColor: Colors.white70,
|
||||
inactiveTickMarkColor: Colors.black,
|
||||
overlayColor: Colors.black12,
|
||||
thumbColor: Colors.deepPurple,
|
||||
rangeThumbShape: _CustomRangeThumbShape(),
|
||||
showValueIndicator: ShowValueIndicator.never,
|
||||
),
|
||||
child: RangeSlider(
|
||||
values: _discreteCustomValues,
|
||||
min: 0.0,
|
||||
max: 200.0,
|
||||
divisions: 5,
|
||||
labels: RangeLabels('${_discreteCustomValues.start.round()}', '${_discreteCustomValues.end.round()}'),
|
||||
onChanged: (RangeValues values) {
|
||||
setState(() {
|
||||
_discreteCustomValues = values;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
const Text('Discrete with Custom Theme'),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -46,11 +46,15 @@ class TabbedComponentDemoScaffold extends StatelessWidget {
|
|||
this.title,
|
||||
this.demos,
|
||||
this.actions,
|
||||
this.isScrollable = true,
|
||||
this.showExampleCodeAction = true,
|
||||
});
|
||||
|
||||
final List<ComponentDemoTabData> demos;
|
||||
final String title;
|
||||
final List<Widget> actions;
|
||||
final bool isScrollable;
|
||||
final bool showExampleCodeAction;
|
||||
|
||||
void _showExampleCode(BuildContext context) {
|
||||
final String tag = demos[DefaultTabController.of(context).index].exampleCodeTag;
|
||||
|
@ -93,29 +97,28 @@ class TabbedComponentDemoScaffold extends StatelessWidget {
|
|||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(title),
|
||||
actions: (actions ?? <Widget>[])..addAll(
|
||||
<Widget>[
|
||||
Builder(
|
||||
builder: (BuildContext context) {
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.library_books, semanticLabel: 'Show documentation'),
|
||||
onPressed: () => _showApiDocumentation(context),
|
||||
);
|
||||
},
|
||||
),
|
||||
Builder(
|
||||
builder: (BuildContext context) {
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.code),
|
||||
tooltip: 'Show example code',
|
||||
onPressed: () => _showExampleCode(context),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: (actions ?? <Widget>[])..add(
|
||||
Builder(
|
||||
builder: (BuildContext context) {
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.library_books, semanticLabel: 'Show documentation'),
|
||||
onPressed: () => _showApiDocumentation(context),
|
||||
);
|
||||
},
|
||||
),
|
||||
)..addAll(showExampleCodeAction ? <Widget>[
|
||||
Builder(
|
||||
builder: (BuildContext context) {
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.code),
|
||||
tooltip: 'Show example code',
|
||||
onPressed: () => _showExampleCode(context),
|
||||
);
|
||||
},
|
||||
),
|
||||
] : <Widget>[]),
|
||||
bottom: TabBar(
|
||||
isScrollable: true,
|
||||
isScrollable: isScrollable,
|
||||
tabs: demos.map<Widget>((ComponentDemoTabData data) => Tab(text: data.tabName)).toList(),
|
||||
),
|
||||
),
|
||||
|
|
Loading…
Reference in a new issue