揭开Flutter Slider中Shapes的神秘面纱

4a71fc865a1797e3ac561c100cc4bb78.png

点击上方蓝字关注我,知识会给你力量

35d2f209353e32f30bb5c2d330585970.png

Slider是Flutter中使用非常多的一个组件,通常设计师都会对Slider做很多的自定义设计,在Android中,我们其实是很难通过配置xml来改变Slider的外观的,而在Flutter中,我们可以很方便的组合整个实现,当然,前提是你需要对Slider的整体概念有个清晰的认识。

下面这张图是Slider的一个基本组成,这里已经融合了一些基础的设计元素,所以,将它作为一个整体,是不错的选择。

b30286a9572daee1c3531d3e1eccb942.png

它主要组件定义如下:

  • thumb:是用户拖动时水平滑动的Shape。

  • track:是Sliderthumb滑动的线。 

  • overlay:拖动时按下thumb时出现的光晕效果。

  • tick marks:使用离散分割时绘制的有规律间隔的标记。

  • value indicator:用户拖动thumb时出现,指示所选的值。

我们通过一个Flutter中的默认Slider来开始我们的改造之旅。

SliderTheme(
        data: SliderTheme.of(context).copyWith(
          activeTrackColor: Colors.red[700],
          inactiveTrackColor: Colors.red[100],
          trackShape: const RoundedRectSliderTrackShape(),
          trackHeight: 4.0,
          thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 12.0),
          thumbColor: Colors.redAccent,
          overlayColor: Colors.red.withAlpha(32),
          overlayShape: const RoundSliderOverlayShape(overlayRadius: 28.0),
          tickMarkShape: const RoundSliderTickMarkShape(),
          activeTickMarkColor: Colors.red[700],
          inactiveTickMarkColor: Colors.red[100],
          valueIndicatorShape: const PaddleSliderValueIndicatorShape(),
          valueIndicatorColor: Colors.redAccent,
          valueIndicatorTextStyle: const TextStyle(color: Colors.white),
        ),
        child: Slider(
          value: _value,
          min: 0,
          max: 100,
          divisions: 10,
          label: '$_value',
          onChanged: (value) {
            setState(() => _value = value);
          },
        ),
      )

效果如下:

29a5ae5d178901a9a88f14cafc434eb1.gif

我们不难发现,对于上文讨论的每个组件,SliderThemeData 对象都会采用某种Shape。 此外,它还包含活动轨迹颜色(activeTrackColor)、overlay颜色(overlayColor)、轨迹高度(trackHeight)等参数,这些参数可以帮助我们自定义组件的某个方面。 虽然不言自明,但我们还是要更好地理解其中的一些属性。

  • activeTrackColor : activeTrack 通常是从最小值到大thumb的轨道边。 我们应为此属性指定一个颜色对象。 

  • inactiveTrackColor : 非活动轨迹通常是从thumb到最大值的轨迹。 我们将为该属性指定一个颜色对象。 

  • trackShape:接收Sliderthumb滑动轨道的Shape。 RoundedRectSliderTrackShape 是隐式分配给它的。 稍后我们将讨论如何自定义该Shape。 

  • trackHeight:以像素为单位定义轨道的高度。 接收一个 double 类型的值。

  • thumbShape : 接收thumb的Shape。 RoundSliderThumbShape 已隐式赋值给它。 我们可以通过传递半径值来改变thumb的大小。 我们将自定义此Shape,以实现最初的目标。 

  • OverlayShape:thumb后面的光晕效果Shape。 RoundSliderOverlayShape 已隐式分配给它。 可以将半径值传递给Shape。 

  • tickMarkShape:在Slider中,通过离散值选择显示刻度线。 这些标记也是可以自定义的Shape。 RoundSliderTickMarkShape 是隐式赋值。 同样,半径值也可以传递给它。 

  • valueIndicatorShape:当用户拖动thumb时,valueIndicator 就会显示,以指示所选的值。 这有助于设置显示值的Shape。PaddleSliderValueIndicatorShape 是Slider值指示器的默认Shape。 

  • valueIndicatorTextStyle :该参数用于为指示器上显示的文本设置 TextStyle。

这并不是 SliderThemeData 类所有属性的详尽列表。 其中提到的属性涵盖了大多数使用情况。 其他大部分属性用于处理部件的禁用状态。

thumb, track, overlay, tick marks,所有这些组件都只是Shape而已。 Material Slider的默认thumbShape是 RoundSliderThumbShape,这是用于构建thumb的默认类。 在查看该类的源代码时,我们会发现该类使用Canvas来绘制Shape。 这意味着我们几乎可以绘制任何自定义Shape、文本等。 它属于 Flutter 的自定义绘画领域。,只是略有不同。 它扩展了 SliderComponentShape 类。

165936688cd484ca4711f510c2fe572d.png

这里值得注意的是 2 个重载方法,即 getPreferredSize 和 paint 方法。 

paint 方法直接用于自定义绘制。 由于我们扩展的是 SliderComponentShape 类,因此绘制方法会接收所有相关数据,以便构建thumb、overlay或任何其他扩展 SliderComponentShape 类的组件。 我们不会再浪费时间解释单个属性,因为示例总是比概念更好。 因此,让我们通过创建开始时的示例来学习实际的生产代码。

7b52ce5e5b9eb44fb25c3379345db9f2.png

Slider with Continuous Values

44f7b984a78a412a59d544080b7fbb07.png

Slider with Discrete Divisions.

Slider with Rectangular Thumb

这正是我们从一开始就想制作的Slider。 但与基本Slider相比,它们有什么特别之处呢? 有以下几点: 

  • thumb本身包含一个始终可见的数值指示器,与内置数值指示器不同的是,内置数值指示器只有在指针被拖动或按下时才会弹出。 

  • 示例中显示的是圆角矩形thumb。 

  • 只显示track,track的活动边和非活动边有不同的颜色。 为此,我们将活动和非活动轨迹颜色的不透明度都设置为零。 

  • slider被放置在具有圆角和渐变背景的矩形内。 这是通过将Slider包裹在一个带有 borderRadius 和 LinearGradient 的容器中实现的。

我们先来看看带数值指示器的Thumb。

classCustomSliderThumbCircleextendsSliderComponentShape {
  final double thumbRadius;
  final int min;
  final int max;
constCustomSliderThumbCircle({
    required this.thumbRadius,
    this.min = 0,
    this.max = 10,
  });
  @override
  Size getPreferredSize(bool isEnabled, bool isDiscrete) => Size.fromRadius(thumbRadius);
  @override
  voidpaint(
    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 doublevalue,
    required double textScaleFactor,
    required Size sizeWithOverflow,
  }) {
    final Canvas canvas = context.canvas;
    final paint = Paint()
      ..color = Colors.white //Thumb Background Color
      ..style = PaintingStyle.fill;
    TextSpan span = TextSpan(
      style: TextStyle(
        fontSize: thumbRadius * .8,
        fontWeight: FontWeight.w700,
        color: sliderTheme.thumbColor, //Text Color of Value on Thumb
      ),
      text: getValue(value),
    );
    TextPainter tp = TextPainter(text: span, textAlign: TextAlign.center, textDirection: TextDirection.ltr);
    tp.layout();
    Offset textCenter = Offset(center.dx - (tp.width / 2), center.dy - (tp.height / 2));
    canvas.drawCircle(center, thumbRadius * .9, paint);
    tp.paint(canvas, textCenter);
  }
String getValue(doublevalue) {
    return (min + (max - min) * value).round().toString();
  }
}

Custom Thumb的属性包括thumbRadius、Slider的最小值和最大值,这些都是在Thumb上绘制所必需的。

我们知道,实际的自定义绘制是在 paint() 中进行的。 因此,它也为我们提供了绘制组件所需的所有相关数据。 其中一些重要数据包括: 

  • context:我们使用上下文提取Canvas。 

  • center:帮助我们对齐组件Shape。 

  • isDiscrete:布尔值,表明Slider是否使用divisions。 

  • sliderTheme:对我们用Slider包装的 SliderThemeData 对象的引用,我们可以从中提取重要的主题数据。 我们可以从中提取关键的主题数据。 

  • value : 给出Slider的值,归一化为 0.0 至 1.0 的范围。 我们可以根据范围轻松更改原点。 

  • Canvas:是我们的画板。

  • paint:是我们的画笔,我们可以使用填充、描边等。 也可用于设置文本样式。

上面代码中的一些要点可能需要解释一下:

  • 创建paint对象是为了向Shape填充颜色。 我们还可以用它来添加描边等。 

  • 我们使用名为 getValue() 的方法将 paint() 中的归一化值转换为我们的范围。

  • 我们需要找到一个偏移量来将文本绘制在绝对中心。 因此需要额外的代码来计算出正确的文本中心偏移量。 

下面是使用 paint() 中的中心偏移量作为文本偏移量时的效果。

6d4e5d9f5109bffa47c297cd7159394f.png

然后使用 canvas.draw 函数绘制Shape,之后使用 textPainter 对象绘制文本。 

下面是圆角矩形thumb的代码

classCustomSliderThumbRectextendsSliderComponentShape{
finaldouble thumbRadius;
finalint thumbHeight;
finalint min;
finalint max;
constCustomSliderThumbRect({
    required this.thumbRadius,
    required this.thumbHeight,
    required this.min,
    required this.max,
  });
@override
Size getPreferredSize(bool isEnabled, bool isDiscrete)=> Size.fromRadius(thumbRadius);
@override
voidpaint(
    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,
  }){
    final Canvas canvas = context.canvas;
    final rRect = RRect.fromRectAndRadius(
      Rect.fromCenter(center: center, width: thumbHeight * 1.2, height: thumbHeight * .6),
      Radius.circular(thumbRadius * .4),
    );
    final paint = Paint()
      ..color = sliderTheme.activeTrackColor! //Thumb Background Color
      ..style = PaintingStyle.fill;
    TextSpan span = TextSpan(
      style: TextStyle(fontSize: thumbHeight * .3, fontWeight: FontWeight.w700, color: sliderTheme.thumbColor, height: 1),
      text: getValue(value),
    );
    TextPainter tp = TextPainter(text: span, textAlign: TextAlign.left, textDirection: TextDirection.ltr);
    tp.layout();
    Offset textCenter = Offset(center.dx - (tp.width / 2), center.dy - (tp.height / 2));
    canvas.drawRRect(rRect, paint);
    tp.paint(canvas, textCenter);
  }
String getValue(double value){
    return (min + (max - min) * value).round().toString();
  }
}

为简洁起见,我们无法创建所有组件Shape的自定义实现。 但我可以肯定的是,本文已经提供了足够的信息,让你可以自定义 Material Slider Widget 的每一个微小部分。 下面我将附上Slider本身的代码。

classSliderWidgetextendsStatefulWidget {
  final double sliderHeight;
  final int min;
  final int max;
  final bool fullWidth;
  const SliderWidget({
    super.key,
    this.sliderHeight = 48,
    this.max = 10,
    this.min = 0,
    this.fullWidth = false,
  });
  @override
  SliderWidgetState createState() => SliderWidgetState();
}
classSliderWidgetStateextendsState<SliderWidget> {
  double _value = 0;
  @override
  Widget build(BuildContext context) {
    double paddingFactor = .2;
    if (widget.fullWidth) paddingFactor = .3;
    return Container(
      width: widget.fullWidth ? double.infinity : (widget.sliderHeight) * 5.5,
      height: (widget.sliderHeight),
      decoration: BoxDecoration(
        borderRadius: BorderRadius.all(Radius.circular((widget.sliderHeight * .3))),
        gradient: const LinearGradient(
          colors: [
            Color(0xFF00c6ff),
            Color(0xFF0072ff),
          ],
          begin: FractionalOffset(0.0, 0.0),
          end: FractionalOffset(1.0, 1.00),
          stops: [0.0, 1.0],
          tileMode: TileMode.clamp,
        ),
      ),
      child: Padding(
        padding: EdgeInsets.fromLTRB(widget.sliderHeight * paddingFactor, 2, widget.sliderHeight * paddingFactor, 2),
        child: Row(
          children: <Widget>[
            Text(
              '
${widget.min}',
              textAlign: TextAlign.center,
              style: TextStyle(
                fontSize: widget.sliderHeight * .3,
                fontWeight: FontWeight.w700,
                color: Colors.white,
              ),
            ),
            SizedBox(width: widget.sliderHeight * .1),
            Expanded(
              child: Center(
                child: SliderTheme(
                  data: SliderTheme.of(context).copyWith(
                    activeTrackColor: Colors.white.withOpacity(1),
                    inactiveTrackColor: Colors.white.withOpacity(.5),
                    trackHeight: 4.0,
                    thumbShape: CustomSliderThumbRect(
                      thumbRadius: widget.sliderHeight * .4,
                      min: widget.min,
                      max: widget.max,
                      thumbHeight: 30,
                    ),
                    overlayColor: Colors.white.withOpacity(.4),
                    activeTickMarkColor: Colors.white,
                    inactiveTickMarkColor: Colors.red.withOpacity(.7),
                  ),
                  child: Slider(
                      value: _value,
                      onChanged: (value) {
                        setState(() => _value = value);
                      }),
                ),
              ),
            ),
            SizedBox(width: widget.sliderHeight * .1),
            Text(
              '$
{widget.max}',
              textAlign: TextAlign.center,
              style: TextStyle(
                fontSize: widget.sliderHeight * .3,
                fontWeight: FontWeight.w700,
                color: Colors.white,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

翻译并修改自:https://medium.com/flutter-community/flutter-sliders-demystified-4b3ea65879c

向大家推荐下我的网站 https://www.yuque.com/xuyisheng  点击原文一键直达

专注 Android-Kotlin-Flutter 欢迎大家访问

往期推荐

本文原创公众号:群英传,授权转载请联系微信(Tomcat_xu),授权后,请在原创发表24小时后转载。

< END >

作者:徐宜生

更文不易,点个“三连”支持一下👇

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值