介绍
当你接到产品需求,做一个滑块选择器,可能是这样的
当咱们运用flutter默许的slider时看到的效果确是这样的
这一看差距太大了,这原生的Flutter Slider肯定无法运用了,于是咱们开始寻找开源组件
比方:pub.dev/packages/sy…
可是如果这些还是无法满意里,或者你不想引进一些第三方库(可能因为开源协议不友好之类,或者有些细节无法满意),那现在我来教你如何快速完成自己的Slider
官方Slider的完成
在看源码的时候,还发现了官方的bug,顺便帮Flutter官方修正了一下,详细可以检查我的别的一篇文章# 修正Flutter官方Slider bug并成功合入的经历
flutter官方框架的Slider的完成基本功能是没问题的,仅仅制作的和规划不符合,所以咱们可以先看一下他的完成,然后再看咱们需求做什么。
这儿先看一下一个Slider分哪几个部分,咱们先简略的从下图来分
- 拖动按钮
- 按下时显示的浮层
- 轨迹
paint
下面咱们来看Slider的制作部分,咱们直接跟进到源码:(这儿咱们需求具备的基础知识,制作时再RenderObject的paint中,咱们直接找到Slider关联的RenderObject)
这儿很简单咱们找到_RenderSlider
的paint
,这儿的代码其实也不算长,逻辑也很清楚,要害逻辑我现已中文加上了注释
@override
void paint(PaintingContext context, Offset offset) {
final double value = _state.positionController.value;
final double? secondaryValue = _secondaryTrackValue;
// The visual position is the position of the thumb from 0 to 1 from left
// to right. In left to right, this is the same as the value, but it is
// reversed for right to left text.
final double visualPosition;
final double? secondaryVisualPosition;
switch (textDirection) {
case TextDirection.rtl:
visualPosition = 1.0 - value;
secondaryVisualPosition = (secondaryValue != null) ? (1.0 - secondaryValue) : null;
break;
case TextDirection.ltr:
visualPosition = value;
secondaryVisualPosition = (secondaryValue != null) ? secondaryValue : null;
break;
}
//获取轨迹的尺度
final Rect trackRect = _sliderTheme.trackShape!.getPreferredRect(
parentBox: this,
offset: offset,
sliderTheme: _sliderTheme,
isDiscrete: isDiscrete,
);
//按钮的方位
final Offset thumbCenter = Offset(trackRect.left + visualPosition * trackRect.width, trackRect.center.dy);
if (isInteractive) {
final Size overlaySize = sliderTheme.overlayShape!.getPreferredSize(isInteractive, false);
overlayRect = Rect.fromCircle(center: thumbCenter, radius: overlaySize.width / 2.0);
}
final Offset? secondaryOffset = (secondaryVisualPosition != null) ? Offset(trackRect.left + secondaryVisualPosition * trackRect.width, trackRect.center.dy) : null;
//制作轨迹
_sliderTheme.trackShape!.paint(
context,
offset,
parentBox: this,
sliderTheme: _sliderTheme,
enableAnimation: _enableAnimation,
textDirection: _textDirection,
thumbCenter: thumbCenter,
secondaryOffset: secondaryOffset,
isDiscrete: isDiscrete,
isEnabled: isInteractive,
);
if (!_overlayAnimation.isDismissed) {
//制作浮层
_sliderTheme.overlayShape!.paint(
context,
thumbCenter,
activationAnimation: _overlayAnimation,
enableAnimation: _enableAnimation,
isDiscrete: isDiscrete,
labelPainter: _labelPainter,
parentBox: this,
sliderTheme: _sliderTheme,
textDirection: _textDirection,
value: _value,
textScaleFactor: _textScaleFactor,
sizeWithOverflow: screenSize.isEmpty ? size : screenSize,
);
}
if (isDiscrete) {
final double tickMarkWidth = _sliderTheme.tickMarkShape!.getPreferredSize(
isEnabled: isInteractive,
sliderTheme: _sliderTheme,
).width;
final double padding = trackRect.height;
final double adjustedTrackWidth = trackRect.width - padding;
// If the tick marks would be too dense, don't bother painting them.
if (adjustedTrackWidth / divisions! >= 3.0 * tickMarkWidth) {
final double dy = trackRect.center.dy;
for (int i = 0; i <= divisions!; i++) {
final double value = i / divisions!;
// The ticks are mapped to be within the track, so the tick mark width
// must be subtracted from the track width.
final double dx = trackRect.left + value * adjustedTrackWidth + padding / 2;
final Offset tickMarkOffset = Offset(dx, dy);
//制作刻度
_sliderTheme.tickMarkShape!.paint(
context,
tickMarkOffset,
parentBox: this,
sliderTheme: _sliderTheme,
enableAnimation: _enableAnimation,
textDirection: _textDirection,
thumbCenter: thumbCenter,
isEnabled: isInteractive,
);
}
}
}
if (isInteractive && label != null && !_valueIndicatorAnimation.isDismissed) {
if (showValueIndicator) {
_state.paintValueIndicator = (PaintingContext context, Offset offset) {
if (attached) {
//制作指示器
_sliderTheme.valueIndicatorShape!.paint(
context,
offset + thumbCenter,
activationAnimation: _valueIndicatorAnimation,
enableAnimation: _enableAnimation,
isDiscrete: isDiscrete,
labelPainter: _labelPainter,
parentBox: this,
sliderTheme: _sliderTheme,
textDirection: _textDirection,
value: _value,
textScaleFactor: textScaleFactor,
sizeWithOverflow: screenSize.isEmpty ? size : screenSize,
);
}
};
}
}
//制作按钮
_sliderTheme.thumbShape!.paint(
context,
thumbCenter,
activationAnimation: _overlayAnimation,
enableAnimation: _enableAnimation,
isDiscrete: isDiscrete,
labelPainter: _labelPainter,
parentBox: this,
sliderTheme: _sliderTheme,
textDirection: _textDirection,
value: _value,
textScaleFactor: textScaleFactor,
sizeWithOverflow: screenSize.isEmpty ? size : screenSize,
);
}
其实咱们看到官方的规划非常的巧妙
Slider的paint拆分红多个Shape来分别制作。既然实践制作是若干个shape,那么咱们只需求从头完成这些shape就好了,整体划分了五个shape
- overlayShape, //滑块按下的浮层显示
- tickMarkShape, //单滑块的刻度
- thumbShape, //单滑块的按钮
- trackShape, //单滑块的轨迹
- valueIndicatorShape, //单滑块指示器
SliderTheme
首要咱们需求知道这些shape是从哪里取的,比方:thumbShape
,也很直观从_sliderTheme.thumbShape
。下面咱们需求看看sliderTheme是什么东西了。这时咱们从网上可以搜到一堆
SliderTheme(
data: SliderTheme.of(context).copyWith(activeTrackColor: Colors.red),
child: Slider(
value: .5,
onChanged: (value) {},
),
)
看到界说,这么多自界说参数,shape相关的部分我做了注释说明
const SliderThemeData({
this.trackHeight,
this.activeTrackColor,
this.inactiveTrackColor,
this.secondaryActiveTrackColor,
this.disabledActiveTrackColor,
this.disabledInactiveTrackColor,
this.disabledSecondaryActiveTrackColor,
this.activeTickMarkColor,
this.inactiveTickMarkColor,
this.disabledActiveTickMarkColor,
this.disabledInactiveTickMarkColor,
this.thumbColor,
this.overlappingShapeStrokeColor,
this.disabledThumbColor,
this.overlayColor,
this.valueIndicatorColor,
this.overlayShape, //滑块按下的浮层显示
this.tickMarkShape, //单滑块的刻度
this.thumbShape, //单滑块的按钮
this.trackShape, //单滑块的轨迹
this.valueIndicatorShape, //单滑块指示器
this.rangeTickMarkShape, // 双滑块的刻度
this.rangeThumbShape, //双滑块的按钮
this.rangeTrackShape, //双滑块的轨迹
this.rangeValueIndicatorShape, //双滑块的指示器
this.showValueIndicator, // 是否显示指示器
this.valueIndicatorTextStyle,
this.minThumbSeparation,
this.thumbSelector,
this.mouseCursor,
});
自界说Shape
比方咱们需求自界说轨迹,咱们只需求参阅默许的完成,然后自己界说个就好,比方咱们完成这样的款式
trackShape 轨迹制作
咱们只需求把源码(RoundedRectSliderTrackShape)copy处理稍微修正一下就好了。主要是一些canvas的操作
款式一
///
///Slider轨迹制作
///
class TDRoundedRectSliderTrackShape extends SliderTrackShape with BaseSliderTrackShape {
/// Create a slider track that draws two rectangles with rounded outer edges.
const TDRoundedRectSliderTrackShape();
@override
void paint(
PaintingContext context,
Offset offset, {
required RenderBox parentBox,
required SliderThemeData sliderTheme,
required Animation<double> enableAnimation,
required TextDirection textDirection,
required Offset thumbCenter,
Offset? secondaryOffset,
bool isDiscrete = false,
bool isEnabled = false,
double additionalActiveTrackHeight = 2,
}) {
assert(sliderTheme.disabledActiveTrackColor != null);
assert(sliderTheme.disabledInactiveTrackColor != null);
assert(sliderTheme.activeTrackColor != null);
assert(sliderTheme.inactiveTrackColor != null);
assert(sliderTheme.thumbShape != null);
// If the slider [SliderThemeData.trackHeight] is less than or equal to 0,
// then it makes no difference whether the track is painted or not,
// therefore the painting can be a no-op.
if (sliderTheme.trackHeight == null || sliderTheme.trackHeight! <= 0) {
return;
}
assert(sliderTheme is TDSliderThemeData);
sliderTheme as TDSliderThemeData;
// Assign the track segment paints, which are leading: active and
// trailing: inactive.
final activeTrackColorTween =
ColorTween(begin: sliderTheme.disabledActiveTrackColor, end: sliderTheme.activeTrackColor);
final inactiveTrackColorTween =
ColorTween(begin: sliderTheme.disabledInactiveTrackColor, end: sliderTheme.inactiveTrackColor);
final activePaint = Paint()..color = activeTrackColorTween.evaluate(enableAnimation)!;
final inactivePaint = Paint()..color = inactiveTrackColorTween.evaluate(enableAnimation)!;
final Paint leftTrackPaint;
final Paint rightTrackPaint;
switch (textDirection) {
case TextDirection.ltr:
leftTrackPaint = activePaint;
rightTrackPaint = inactivePaint;
break;
case TextDirection.rtl:
leftTrackPaint = inactivePaint;
rightTrackPaint = activePaint;
break;
}
final trackRect = getPreferredRect(
parentBox: parentBox,
offset: offset,
sliderTheme: sliderTheme,
isEnabled: isEnabled,
isDiscrete: isDiscrete,
);
final trackRadius = Radius.circular(trackRect.height / 2);
final activeTrackRadius = Radius.circular((trackRect.height + additionalActiveTrackHeight) / 2);
context.canvas.drawRRect(
RRect.fromLTRBAndCorners(
trackRect.left,
(textDirection == TextDirection.rtl) ? trackRect.top - (additionalActiveTrackHeight / 2) : trackRect.top,
thumbCenter.dx,
(textDirection == TextDirection.rtl) ? trackRect.bottom + (additionalActiveTrackHeight / 2) : trackRect.bottom,
topLeft: (textDirection == TextDirection.ltr) ? activeTrackRadius : trackRadius,
bottomLeft: (textDirection == TextDirection.ltr) ? activeTrackRadius : trackRadius,
),
leftTrackPaint,
);
context.canvas.drawRRect(
RRect.fromLTRBAndCorners(
thumbCenter.dx,
(textDirection == TextDirection.rtl) ? trackRect.top - (additionalActiveTrackHeight / 2) : trackRect.top,
trackRect.right,
(textDirection == TextDirection.rtl) ? trackRect.bottom + (additionalActiveTrackHeight / 2) : trackRect.bottom,
topRight: (textDirection == TextDirection.rtl) ? activeTrackRadius : trackRadius,
bottomRight: (textDirection == TextDirection.rtl) ? activeTrackRadius : trackRadius,
),
rightTrackPaint,
);
}
}
款式二
class TDCapsuleRectRangeSliderTrackShape extends RangeSliderTrackShape with TDBaseRangeSliderTrackShape {
final Color trackColorWhenShowScale;
/// Create a slider track with rounded outer edges.
///
/// The middle track segment is the selected range and is active, and the two
/// outer track segments are inactive.
const TDCapsuleRectRangeSliderTrackShape({this.trackColorWhenShowScale = const Color(0xFFE7E7E7)});
@override
Rect getPreferredRect(
{required RenderBox parentBox,
Offset offset = Offset.zero,
required SliderThemeData sliderTheme,
bool isEnabled = false,
bool isDiscrete = false}) {
var rect = super.getPreferredRect(
parentBox: parentBox,
offset: offset,
sliderTheme: sliderTheme,
isEnabled: isEnabled,
isDiscrete: isDiscrete,
);
return Rect.fromLTRB(rect.left + 12, rect.top, rect.right - 12, rect.bottom);
}
@override
void paint(
PaintingContext context,
Offset offset, {
required RenderBox parentBox,
required SliderThemeData sliderTheme,
required Animation<double> enableAnimation,
required Offset startThumbCenter,
required Offset endThumbCenter,
bool isEnabled = false,
bool isDiscrete = false,
required TextDirection textDirection,
double additionalActiveTrackHeight = 3,
}) {
assert(sliderTheme.disabledActiveTrackColor != null);
assert(sliderTheme.disabledInactiveTrackColor != null);
assert(sliderTheme.activeTrackColor != null);
assert(sliderTheme.inactiveTrackColor != null);
assert(sliderTheme.rangeThumbShape != null);
if (sliderTheme.trackHeight == null || sliderTheme.trackHeight! <= 0) {
return;
}
var showScale = (sliderTheme as TDSliderThemeData).showScaleValue;
// Assign the track segment paints, which are left: active, right: inactive,
// but reversed for right to left text.
final activeTrackColorTween = ColorTween(
begin: sliderTheme.disabledActiveTrackColor,
end: sliderTheme.activeTrackColor,
);
final inactiveTrackColorTween = ColorTween(
begin: sliderTheme.disabledInactiveTrackColor,
end: showScale ? trackColorWhenShowScale : sliderTheme.inactiveTrackColor,
);
final activePaint = Paint()..color = activeTrackColorTween.evaluate(enableAnimation)!;
final inactivePaint = Paint()..color = inactiveTrackColorTween.evaluate(enableAnimation)!;
final Offset leftThumbOffset;
final Offset rightThumbOffset;
switch (textDirection) {
case TextDirection.ltr:
leftThumbOffset = startThumbCenter;
rightThumbOffset = endThumbCenter;
break;
case TextDirection.rtl:
leftThumbOffset = endThumbCenter;
rightThumbOffset = startThumbCenter;
break;
}
final thumbSize = sliderTheme.rangeThumbShape!.getPreferredSize(isEnabled, isDiscrete);
final thumbRadius = thumbSize.width / 2;
assert(thumbRadius > 0);
final trackRect = getPreferredRect(
parentBox: parentBox,
offset: offset,
sliderTheme: sliderTheme,
isEnabled: isEnabled,
isDiscrete: isDiscrete,
);
final trackRadius = Radius.circular(trackRect.height / 2);
context.canvas.drawRRect(
RRect.fromLTRBAndCorners(
trackRect.left - 12,
trackRect.top,
trackRect.right + 12,
trackRect.bottom,
topLeft: trackRadius,
bottomLeft: trackRadius,
topRight: trackRadius,
bottomRight: trackRadius,
),
inactivePaint,
);
var activeTrackRadius = Radius.circular(trackRect.height / 2 - additionalActiveTrackHeight);
final inactiveSecondPaint = Paint()..color = sliderTheme.inactiveTrackColor!;
if (showScale) {
context.canvas.drawRRect(
RRect.fromLTRBAndCorners(
trackRect.left - 9,
trackRect.top + additionalActiveTrackHeight,
rightThumbOffset.dx,
trackRect.bottom - additionalActiveTrackHeight,
topLeft: activeTrackRadius,
bottomLeft: activeTrackRadius,
),
inactiveSecondPaint,
);
}
context.canvas.drawRect(
Rect.fromLTRB(
leftThumbOffset.dx,
trackRect.top + additionalActiveTrackHeight,
rightThumbOffset.dx,
trackRect.bottom - additionalActiveTrackHeight,
),
activePaint,
);
if ((sliderTheme).showScaleValue) {
context.canvas.drawRRect(
RRect.fromLTRBAndCorners(
rightThumbOffset.dx,
trackRect.top + additionalActiveTrackHeight,
trackRect.right + 9,
trackRect.bottom - additionalActiveTrackHeight,
topRight: activeTrackRadius,
bottomRight: activeTrackRadius,
),
inactiveSecondPaint,
);
}
}
}
overlay,tickMark,thumb,valueIndicator
思路和trackShape相同这儿就不过多的赘述,详细可参阅文尾补白的源码
RangeSlider
思路与Slider相同,自界说完成所需的shape即可
源码
库房:github.com/TDesignOtea…