04_Flutter自定义Slider滑块
一.Slider控件基本用法
Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Text(
"sliderValue: ${_sliderValue.toInt()}"
),
Slider(
value: _sliderValue,
min: 0,
max: 100,
divisions: 10,
thumbColor: Colors.red,
activeColor: Colors.red,
onChanged: (value) {
setState(() {
_sliderValue = value;
});
}
)
],
)
const Slider({
super.key,
required this.value,
this.secondaryTrackValue,
required this.onChanged,
this.onChangeStart,
this.onChangeEnd,
this.min = 0.0,
this.max = 1.0,
this.divisions,
this.label,
this.activeColor,
this.inactiveColor,
this.secondaryActiveColor,
this.thumbColor,
this.overlayColor,
this.mouseCursor,
this.semanticFormatterCallback,
this.focusNode,
this.autofocus = false,
})
几个比较重要的属性:
- value:slider控件显示的值
- min:slider控件滑动到最左边对应的值,即最小值
- max: slider控件滑动到最右边对应的值,即最大值
- divisions: 最小值到最大值之间被几等分
- activeColor: 滑块划过部分的颜色值,即选中的颜色值
- inactiveColor:滑块未划过部分的颜色值,即为选中的颜色值
- thumbColor:滑块的颜色值
二.如何修改滑块的大小以及滑块轨迹的高度
从上面的示例可以看到,通过Slider控件为我们提供的属性,只支持改变滑块的颜色,以及滑块轨迹的颜色,那么我们想要改变滑块的大小以及滑块轨迹的高度,是不是只能重新自定义呢?
NO! NO! NO!,细心的您在使用Flutter的AppBar时,可能会发现,为AppBar控件指定样式时,除了使用AppBar控件提供的属性外,也可以使用AppBarTheme来为AppBar设置某些特定的样式,既然如此,不妨查看下Flutter sdk的源码与Slider对应的是否有一个叫SliderTheme的控件呢? 嘿嘿,还真有。
final SliderThemeData data;
const SliderTheme({
super.key,
required this.data,
required super.child,
});
const SliderThemeData({
this.trackHeight,
this.thumbShape,
...
});
仔细找SliderThemeData的trackHeight以及thumbShape的属性注释:
/// The height of the [Slider] track.
final double? trackHeight;
/// The shape that will be used to draw the [Slider]'s thumb.
/// The default value is [RoundSliderThumbShape].
final SliderComponentShape? thumbShape;
此处省略…翻译软件的时间:
- trackHeight:[滑块]轨迹的高度
- thumbShape:默认值是一个RoundSliderThumbShape对象
看下RoundSliderThumbShape的源码怎么写的:
const RoundSliderThumbShape({
this.enabledThumbRadius = 10.0,
this.disabledThumbRadius,
this.elevation = 1.0,
this.pressedElevation = 6.0,
});
看到这里就不用做过多的解释了吧😂,因此要修改滑块的大小,可以重新指定thumbShape为RoundSliderThumbShape对象,并设置enabledThumbRadius的值。
Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Text(
"sliderValue: ${_sliderValue.toInt()}"
),
SliderTheme(
data: const SliderThemeData(
trackHeight: 20,
thumbShape: RoundSliderThumbShape(
enabledThumbRadius: 20
)
),
child: Slider(
value: _sliderValue,
min: 0,
max: 100,
divisions: 10,
thumbColor: Colors.red,
activeColor: Colors.red,
onChanged: (value) {
setState(() {
_sliderValue = value;
});
}
)
)
],
)
三.使用本地资源图片作为自定义滑块
既然要自定义滑块,毫无疑问需要从SliderThemeData的thumbShape入手。
final SliderComponentShape? thumbShape;
thumbShape的类型为SliderComponentShape,继续查看SliderComponentShape源码:
abstract class SliderComponentShape {
const SliderComponentShape();
Size getPreferredSize(bool isEnabled, bool isDiscrete);
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,
});
}
因此我们可以定义一个类继承SliderComponentShape,并实现getPreferredSize和paint方法,getPreferredSize控制滑块大小,paint负责把滑块绘制到屏幕上。
- 首先第一步我们需要将本地图片为一个ImageInfo,例如传入一个"lib/assets/images/ic_slider_thumb.png",最后得到一个ImageInfo,这里就直接奉上源码了,其实现也是参考了Image.asset的源码:
typedef AssertsWidgetBuilder = Widget Function(BuildContext context, ImageInfo? imageInfo);
class AssertsImageBuilder extends StatefulWidget {
final String assertsName;
final AssertsWidgetBuilder builder;
const AssertsImageBuilder(
this.assertsName,
{
super.key,
required this.builder,
}
);
State<StatefulWidget> createState() => _AssertsImageBuilderState();
}
class _AssertsImageBuilderState extends State<AssertsImageBuilder> {
ImageInfo? _imageInfo;
void initState() {
super.initState();
_loadAssertsImage().then((value) {
setState(() {
_imageInfo = value;
});
});
}
void didUpdateWidget(covariant AssertsImageBuilder oldWidget) {
super.didUpdateWidget(oldWidget);
if(oldWidget.assertsName != widget.assertsName) {
_loadAssertsImage().then((value) {
setState(() {
_imageInfo = value;
});
});
}
}
Widget build(BuildContext context) {
return widget.builder!.call(context, _imageInfo);
}
Future<ImageInfo?> _loadAssertsImage() {
final Completer<ImageInfo?> completer = Completer<ImageInfo?>();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
final ImageProvider imageProvider = AssetImage(widget.assertsName);
final ImageConfiguration config = createLocalImageConfiguration(context);
final ImageStream stream = imageProvider.resolve(config);
ImageStreamListener? listener;
listener = ImageStreamListener(
(ImageInfo? image, bool sync) {
if (!completer.isCompleted) {
completer.complete(image);
}
SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) {
stream.removeListener(listener!);
});
},
onError: (Object exception, StackTrace? stackTrace) {
stream.removeListener(listener!);
completer.completeError(exception, stackTrace);
},
);
stream.addListener(listener);
});
return completer.future;
}
}
- 自定义SliderComponentShape
import 'package:flutter/material.dart';
import 'dart:ui' as ui;
class ImageSliderThumb extends SliderComponentShape {
final Size size;
final ui.Image? image;
const ImageSliderThumb({
required this.image,
Size? size
}): size = size ?? const Size(20, 20);
Size getPreferredSize(bool isEnabled, bool isDiscrete) {
return size;
}
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}) {
}
}
- 绘制图片滑块
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}) {
//图片中心点
double dx = size.width/2;
double dy = size.height/2;
if(image != null) {
final Rect sourceRect = Rect.fromLTWH(0, 0, image!.width.toDouble(), image!.width.toDouble());
//center会随着滑块的移动而改变,所以这里需要根据center计算图片绘制的位置
double left = center.dx - dx;
double top = center.dy - dy;
double right = center.dx + dx;
double bottom = center.dy + dy;
Rect destinationRect = Rect.fromLTRB(left, top, right, bottom);
final Canvas canvas = context.canvas;
final Paint paint = new Paint();
paint.isAntiAlias = true;
//绘制滑块
canvas.drawImageRect(image!, sourceRect, destinationRect, paint);
}
}
四.怎么使用?
Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Text(
"sliderValue: ${_sliderValue.toInt()}"
),
AssertsImageBuilder(
"lib/assets/images/ic_slider_thumb.png",
builder: (context, imageInfo) {
return SliderTheme(
data: SliderThemeData(
trackHeight: 10,
thumbShape: ImageSliderThumb(
image: imageInfo?.image,
size: const Size(30, 30)
)
),
child: Slider(
value: _sliderValue,
min: 0,
max: 100,
divisions: 10,
thumbColor: Colors.red,
activeColor: Colors.red,
onChanged: (value) {
setState(() {
_sliderValue = value;
});
}
)
);
}
),
],
)