原文链接:Flutter Slider widgets: A deep dive with example - 原文作者 Souvik Biswas
本文采用意译的方式
Slider
是一个基本的 Flutter
挂件 - 可以通过移动 slider
的滑块来选择范围值。在 Flutter
中,有不同类型的 slider
挂件,Flutter
框架中常用的有:
Material Design
组件,它允许你在一个范围值中选中一个值(存在一个滑块 slider thumb
)Slider
相似,但是属于 Cupertino
设计风格。本文,我们将会学到:
Flutter App
中,如何使用这些基本的挂件CustomPainter
,如何自定义 slider
挂件设计现在,我们进入正题。
最基本的 Slider
格式形式如下:
实现上面效果的相关代码如下:
Slider(
min: 0.0,
max: 100.0,
value: _value,
onChanged: (value) {
setState(() {
_value = value;
});
},
)
在 widget class
中,_value
会被初始化:
double _value = 20;
上面我设置的属性,是我们使用 Flutter
构建任何 slider
至少需要用到的属性,但是,不同的 slider
,属性可能有点不同。我们看看这些属性:
min
:用户可以拖动 slider
到左边的最小值(越靠 slider
的左边,数值越小)max
:用户可以拖动 slider
到右边的最大值(越靠 slider
的右边,数值越大)value
:用户通过拖动滑块获取到的 slider
当前值onChanged
:这是个回调函数,当在 slider
轨道上往左或往右拖动滑块,将会调用该函数并返回当前 slider
的位置值在 onChanged
内部,我们通过 setState
来更新 _value
变量:
setState(() {
_value = value;
});
这里,setState
用来更新 UI
,以便于每次更新值都能在 slider
挂件中反映出来。需要注意的是,Slider
应该在 StatefulWidget
中使用 setState
。
这个基本的 slider
挂件使用了 Material Design
风格,这很适合 Android devices
,而 iOS
设备趋向于使用 Cupertino
风格。在 iOS devices
上,更趋向于使用 CupertinoSlider
。
我们可以通过用 CupertinoSlider
挂件替换 Slider
挂件来实现 iOS-style
的 slider
,它们的属性和上面案例的完全一样。
该 slider
的效果如下:
相关的代码如下:
Container(
width: double.maxFinite,
child: CupertinoSlider(
min: 0.0,
max: 100.0,
value: _value,
onChanged: (value) {
setState(() {
_value = value;
});
},
),
)
默认的,Cupertino Slider
不会占用整个屏幕的宽度,所以我们得用 Container
挂件来包含它,如果我们想让其占满屏幕的宽度,需要提供一个值为 double.maxFinite
的宽度。
Slider
和 CupertinoSlider
都只允许我们在指定的范围选定一个值,但是,如果我们想选中两个值,可以考虑使用 RangeSlider
挂件。
RangeSlider
挂件也是遵循 Material Design
风格,它有两个滑块,控制开始值和结束值。在这个挂件中,没有 value
属性;相反的,有 values
属性,类型是 RangeValues
。
基本的 RangeSlider
挂件长这样👇
相关代码如下:
RangeSlider(
min: 0.0,
max: 100.0,
values: RangeValues(_startValue, _endValue),
onChanged: (values) {
setState(() {
_startValue = values.start;
_endValue = values.end;
});
},
)
RangeValues
需要两个输入值:开始值(_startValue
提供)和结束值(_endValue
提供)。我们可以在 widget class
内定义这两个变量,就像下面这样:
double _startValue = 20.0;
double _endValue = 90.0;
通过这些值来运行应用,slider
两滑块将会被赋予初始值。在 Range Slider
中,回调函数 onChanged
也会返回 RangeValues
,方便我们用来更新两滑块的位置:
setState(() {
_startValue = values.start;
_endValue = values.end;
});
上面我们讨论的三个 slider
挂件,都有一些属性供我们自定义其颜色。
基础的 Slider
挂件有三个属性来设置颜色:
activeColor
:将颜色应用到滑块轨道的活动部分inactiveColor
:将颜色应用到滑块轨道的非活动部分thumbColor
:将颜色应用在滑块(指示块)我们可以实现这个 Slider
颜色组合,使用下面的代码👇
Slider(
min: 0.0,
max: 100.0,
activeColor: Colors.purple,
inactiveColor: Colors.purple.shade100,
thumbColor: Colors.pink,
value: _value,
onChanged: (value) {
setState(() {
_value = value;
});
},
)
相似的,我们可以更改这些属性,来自定义 Slider
颜色,下面就是些例子:
如果我们使用 CupertinoSlider
挂件,我们只需要自定义两个颜色属性:
activeColor
thumbColor
下面就是一个自定义 Cupertino
挂件的例子:
我们可以使用下面的代码来构建上面自定义 iOS-style
的 slider
:
Container(
width: double.maxFinite,
child: CupertinoSlider(
min: 0.0,
max: 100.0,
value: _value,
activeColor: CupertinoColors.activeGreen,
thumbColor: CupertinoColors.systemPink,
divisions: 10,
onChanged: (value) {
setState(() {
_value = value;
});
},
),
)
当然,RangeSlider
挂件也允许我们使用两个属性自定义,但是和 Cupertino Slider
有个不同:
activeColor
inactiveColor
下面是一个自定义 Range Slider
的例子:
上面👆的 slider
通过下面的代码构建:
RangeSlider(
min: 0.0,
max: 100.0,
activeColor: widget.activeColor,
inactiveColor: widget.inactiveColor,
values: RangeValues(_startValue, _endValue),
onChanged: (values) {
setState(() {
_startValue = values.start;
_endValue = values.end;
});
},
)
接下来,我们将讨论更加复杂的自定义场景,然后将其应用在 sliders
上。
通常的,slider
挂件是返回小数的值,因为它们默认是连续的。但是,如果我们只需要离散的值(即,没有任何小数位的整数),可以使用属性 divisions
。
label
属性通常被用来和离散的值配合使用。会在滑块上显示选中的值。
当设定 divisions
和 label
属性后,基本的 Slider
挂件可能像下面这样:
代码如下:
Slider(
min: 0.0,
max: 100.0,
value: _value,
divisions: 10,
label: '${_value.round()}',
onChanged: (value) {
setState(() {
_value = value;
});
},
)
在 CupertinoSlider
挂件中,我们可以设定 divisions
,但是不支持属性 label
。
RangeSlider
挂件有和 Slider
挂件相似的属性:divisions
是用来展示离散的值,labels
将会被使用,因为有两个滑块(指示器)。labels
属性将赋予类型为 RangeLabels
的值。
在应用了 divisons
和 labels
之后,Range Slider
就像下面这样:
相关代码如下:
RangeSlider(
min: 0.0,
max: 100.0,
divisions: 10,
labels: RangeLabels(
_startValue.round().toString(),
_endValue.round().toString(),
),
values: RangeValues(_startValue, _endValue),
onChanged: (values) {
setState(() {
_startValue = values.start;
_endValue = values.end;
});
},
)
在一些场景中,我们需要知道 slider
当前的状态(它是否是空闲的,是否已经被拖动过,是否正在被拖动)。三种 slider
都有一些对应的回调函数帮我们实现。如下:
onChanged
:当用户拖动滑块,就会调用,并更新其值onChangeStart
:当用户开始拖拽时回调。这个回调用来表明用户已经开始拖动,可以被用来更新任何相关的 UI
onChangeEnd
:当用户停止拖拽时回调。这个回调用来表明用户已经停止拖动,可以被用来更新任何相关的 UI
上面列出的三个回调,只有 onChanged
应该被用来更新 slider
值。
这里是一个简单的例子,用户用这三个回调函数来更新 Text
挂件:
上面动图相关代码如下:
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Slider(
min: 0.0,
max: 100.0,
value: _value,
divisions: 10,
onChanged: (value) {
setState(() {
_value = value;
_status = 'active (${_value.round()})';
_statusColor = Colors.green;
});
},
onChangeStart: (value) {
setState(() {
_status = 'start';
_statusColor = Colors.lightGreen;
});
},
onChangeEnd: (value) {
setState(() {
_status = 'end';
_statusColor = Colors.red;
});
},
),
Text(
'Status: $_status',
style: TextStyle(color: _statusColor),
),
],
)
在类的内部,会初始化一些变量,如下:
double _value = 20;
String _status = 'idle';
Color _statusColor = Colors.amber;
这些变量会根据回调函数更新,并且通过 setState
调用来更新 Text
挂件。
现在,我们更深入一些自定义 sliders
的内容。我们可以通过用 SliderTheme
包裹 Slider
挂件来解锁这些自定义内容,这可以让我们通过具体的属性来自定义 slider
各个方面。
让我们构建下面的 slider
:
SliderTheme
有很多的属性,但是我们构建上面效果的属性只需如下:
trackHeight
:指定整个轨道的高度,不管是活跃或者非活跃的轨道部分trackShape
:指定轨道的两端是否是圆形的,应用在活跃或者非活跃的轨道部分。使用 RoundedRectSliderTrackShape
可以有一个很好的圆形边界。activeTrackColor
:指定轨道活跃部分的颜色,在上面的例子中是最左部分,从滑块最小值位置到滑块当前值位置inactiveTrackColor
:指定轨道非活跃部分的颜色,在上面的例子中是最右边部分,从滑块当前值位置到到最大值位置thumbShape
:指定 slider thumb
的形状。RoundSliderThumbShape
表明是一个完全圆形的 thumb
。thumb
的大小及其按压的高度都可以在这里设置thumbColor
:指定 slider thumb
应用的颜色overlayColor
:指定 slider thumb
被按压时候,其旁边可见的蒙层的颜色,该蒙层是透明的overlayShape
:指定蒙层的形状和其圆角tickMarkShape
:轨道上的指示分割点,指定应用在滑块轨道蒙层上的形状。当滑块有分割点的时候可见activeTickMarkColor
:指定在轨道活跃部分的分割点的颜色inactiveTickMarkColor
:指定在轨道非活跃部分的分割点的颜色valueIndicatorShape
:指定指示点值的形状,其含有标签(比如文本值),当 slider thumb
处于按压的状态其可见valueIndicatorColor
:指定指示点值的颜色。通常,会应用接近 slider thumb
的颜色,理论上你可以指定任何颜色valueIndicatorTextStyle
:指定滑块中指示点值文本的样式完整的代码如下:
SliderTheme(
data: SliderTheme.of(context).copyWith(
trackHeight: 10.0,
trackShape: RoundedRectSliderTrackShape(),
activeTrackColor: Colors.purple.shade800,
inactiveTrackColor: Colors.purple.shade100,
thumbShape: RoundSliderThumbShape(
enabledThumbRadius: 14.0,
pressedElevation: 8.0,
),
thumbColor: Colors.pinkAccent,
overlayColor: Colors.pink.withOpacity(0.2),
overlayShape: RoundSliderOverlayShape(overlayRadius: 32.0),
tickMarkShape: RoundSliderTickMarkShape(),
activeTickMarkColor: Colors.pinkAccent,
inactiveTickMarkColor: Colors.white,
valueIndicatorShape: PaddleSliderValueIndicatorShape(),
valueIndicatorColor: Colors.black,
valueIndicatorTextStyle: TextStyle(
color: Colors.white,
fontSize: 20.0,
),
),
child: Slider(
min: 0.0,
max: 100.0,
value: _value,
divisions: 10,
label: '${_value.round()}',
onChanged: (value) {
setState(() {
_value = value;
});
},
),
)
在 SliderTheme
上还有很多其他的属性,但是这些属性够大多数用户进行自定义了。感兴趣,可以尝试其他的属性,你可以走得更远。
CustomPainter
设计自定义 sliders
SliderTheme
允许我们从 Flutter
预设的设计修改滑块组件。如果你想在滑块上给盒外面定制款式,CustomPainter
就会派上用场。
我们可以为不同的 slider
组件创建自己的设计(比如分割标记,slider thumb
,滑轨等),并且为这些组件分配它们形状。
如下,我们将在 Slider
挂件上创建 slider thumb
自定义形状👇
为了创建该多边形的 slider thumb
,我们需要在继承 SliderComponentShape
类的子类中去生成这个形状:
class PolygonSliderThumb extends SliderComponentShape {
@override
Size getPreferredSize(bool isEnabled, bool isDiscrete) {
// Define size here
}
@override
void paint(
PaintingContext context,
Offset center, {
required Animation<double> activationAnimation,
required Animation<double> enableAnimation,
required bool isDiscrete,
required TextPainter labelPainter,
required RenderBox parentBox,
required SliderThemeData sliderTheme,
required TextDirection textDirection,
required double value,
required double textScaleFactor,
required Size sizeWithOverflow,
}) {
// Define the slider thumb design here
}
}
当继承 SliderComponentShape
类,我们需要重写下面两个方法:
getPreferredSize()
:这个方法返回 slider thumb
形状的大小paint()
:这个方法生成 slider thumb
的设计我们需要给 PolygonSliderThumb
类传递两个值,thumb
半径的值和当前滑块选中的值:
class PolygonSliderThumb extends SliderComponentShape {
final double thumbRadius;
final double sliderValue;
const PolygonSliderThumb({
required this.thumbRadius,
required this.sliderValue,
});
@override
Size getPreferredSize(bool isEnabled, bool isDiscrete) {
return Size.fromRadius(thumbRadius);
}
// ...
}
在这里,我们使用了 thumbRadius
变量来定义 slider thumb
形状的半径。
现在,让我们在 paint()
方法上定义形状。这跟我们上面用到的 CustomPainter
很类似,它俩都有相同的概念:
canvas
:绘制和创建我们想要的形状的画布paint
:我们用来绘制的画笔我们可以通过 context
来获取到 canvas
对象,并且传递给 paitn()
方法:
final Canvas canvas = context.canvas;
给多边形自定义些常量,比如多边形的变,其内部或者外部的半径,最后计算其角度:
int sides = 4;
double innerPolygonRadius = thumbRadius * 1.2;
double outerPolygonRadius = thumbRadius * 1.4;
double angle = (math.pi * 2) / sides;
绘制操作的顺序应该如下:
第一点是开始绘制,第二点紧随,第三点最后。
绘制外层路径,我们可以使用下面的代码:
final outerPathColor = Paint()
..color = Colors.pink.shade800
..style = PaintingStyle.fill;
var outerPath = Path();
Offset startPoint2 = Offset(
outerPolygonRadius * math.cos(0.0),
outerPolygonRadius * math.sin(0.0),
);
outerPath.moveTo(
startPoint2.dx + center.dx,
startPoint2.dy + center.dy,
);
for (int i = 1; i <= sides; i++) {
double x = outerPolygonRadius * math.cos(angle * i) + center.dx;
double y = outerPolygonRadius * math.sin(angle * i) + center.dy;
outerPath.lineTo(x, y);
}
outerPath.close();
canvas.drawPath(outerPath, outerPathColor);
绘制内层路径,可以使用如下代码:
final innerPathColor = Paint()
..color = sliderTheme.thumbColor ?? Colors.black
..style = PaintingStyle.fill;
var innerPath = Path();
Offset startPoint = Offset(
innerPolygonRadius * math.cos(0.0),
innerPolygonRadius * math.sin(0.0),
);
innerPath.moveTo(
startPoint.dx + center.dx,
startPoint.dy + center.dy,
);
for (int i = 1; i <= sides; i++) {
double x = innerPolygonRadius * math.cos(angle * i) + center.dx;
double y = innerPolygonRadius * math.sin(angle * i) + center.dy;
innerPath.lineTo(x, y);
}
innerPath.close();
canvas.drawPath(innerPath, innerPathColor);
最后,使用下面代码绘制文本值:
TextSpan span = new TextSpan(
style: new TextStyle(
fontSize: thumbRadius,
fontWeight: FontWeight.w700,
color: Colors.white,
),
text: sliderValue.round().toString(),
);
TextPainter tp = new TextPainter(
text: span,
textAlign: TextAlign.center,
textDirection: TextDirection.ltr,
);
tp.layout();
Offset textCenter = Offset(
center.dx - (tp.width / 2),
center.dy - (tp.height / 2),
);
tp.paint(canvas, textCenter);
最后,我们就可以将自定义的 slider thumb
应用到 SliderTheme
上了:
SliderTheme(
data: SliderTheme.of(context).copyWith(
thumbShape: PolygonSliderThumb(
thumbRadius: 16.0,
sliderValue: _value,
),
),
child: Slider(...)
)
我们不会介绍其他 slider
组件的构建步骤,但是我们可以使用构建多边形 slider thumb
的这些概念,去创建属于自己风格的 slider
。
本文覆盖了我们所需要掌握的 slider
挂件的概念。现在,是时候使用 Flutter
创建属于自己独一无二的 slider
了。