You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何编辑CustomTrackShape实现自定义轨道形状效果?

CustomTrackShape 自定义滑块轨道实现

我正在为项目设计CustomTrackShape自定义轨道形状,目标设计效果如下:
目标效果示意图
目前已完成大部分代码编写,当前实现效果如下:
当前实现效果
侧边圆角问题后续会自行修复,当前还需要实现三点需求:

  • 将label移动到轨道下方
  • 尽可能支持调整label尺寸参数
  • 自定义trackheight,实现轨道一端高度较小、另一端高度较大的效果

可直接复用的实现代码

所有需求点都已覆盖,相关参数全部开放可配置:

import 'package:flutter/material.dart';

class VariableHeightTrackShape extends SliderTrackShape with BaseSliderTrackShape {
  /// 轨道最小端高度
  final double minTrackHeight;
  /// 轨道最大端高度
  final double maxTrackHeight;
  /// 标签与轨道的垂直间距
  final double labelTopGap;
  /// 标签文字样式
  final TextStyle? labelStyle;
  /// 标签内边距
  final Size labelPadding;

  const VariableHeightTrackShape({
    this.minTrackHeight = 4,
    this.maxTrackHeight = 12,
    this.labelTopGap = 8,
    this.labelStyle,
    this.labelPadding = const Size(12, 6),
  });

  @override
  Rect getPreferredRect({
    required RenderBox parentBox,
    Offset offset = Offset.zero,
    required SliderThemeData sliderTheme,
    bool isEnabled = false,
    bool isDiscrete = false,
  }) {
    // 预留下方标签的绘制空间,避免内容被裁剪
    final labelReserveHeight = (labelStyle?.fontSize ?? 12) + labelTopGap + labelPadding.height;
    final totalTrackAreaHeight = maxTrackHeight + labelReserveHeight;
    return Rect.fromLTWH(
      offset.dx,
      offset.dy + (parentBox.size.height - totalTrackAreaHeight) / 2,
      parentBox.size.width,
      totalTrackAreaHeight,
    );
  }

  @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 isEnabled = false,
    bool isDiscrete = false,
  }) {
    final canvas = context.canvas;
    final trackTotalWidth = parentBox.size.width;
    // 轨道垂直居中,为下方标签留出空间
    final trackVerticalCenter = offset.dy + maxTrackHeight / 2;
    final trackBottomPos = trackVerticalCenter + maxTrackHeight / 2;

    // 绘制变高度梯形轨道
    _drawVariableHeightTrack(
      canvas: canvas,
      trackStart: offset.dx,
      trackEnd: offset.dx + trackTotalWidth,
      verticalCenter: trackVerticalCenter,
      activeColor: sliderTheme.activeTrackColor ?? Colors.blue,
      inactiveColor: sliderTheme.inactiveTrackColor ?? Colors.grey.shade300,
      thumbPosX: thumbCenter.dx,
    );

    // 绘制轨道下方标签
    _drawBottomLabel(
      canvas: canvas,
      thumbCenter: thumbCenter,
      sliderTheme: sliderTheme,
      trackBottom: trackBottomPos,
    );
  }

  /// 绘制一端高一端低的梯形轨道
  void _drawVariableHeightTrack({
    required Canvas canvas,
    required double trackStart,
    required double trackEnd,
    required double verticalCenter,
    required Color activeColor,
    required Color inactiveColor,
    required double thumbPosX,
  }) {
    // 左侧未选中轨道:从左到右高度从min线性增长到max
    final inactiveTrackPath = Path()
      ..moveTo(trackStart, verticalCenter - minTrackHeight / 2)
      ..lineTo(thumbPosX, verticalCenter - maxTrackHeight / 2)
      ..lineTo(thumbPosX, verticalCenter + maxTrackHeight / 2)
      ..lineTo(trackStart, verticalCenter + minTrackHeight / 2)
      ..close();
    canvas.drawPath(inactiveTrackPath, Paint()..color = inactiveColor);

    // 右侧选中轨道:从滑块位置到右端高度从max线性降到min
    final activeTrackPath = Path()
      ..moveTo(thumbPosX, verticalCenter - maxTrackHeight / 2)
      ..lineTo(trackEnd, verticalCenter - minTrackHeight / 2)
      ..lineTo(trackEnd, verticalCenter + minTrackHeight / 2)
      ..lineTo(thumbPosX, verticalCenter + maxTrackHeight / 2)
      ..close();
    canvas.drawPath(activeTrackPath, Paint()..color = activeColor);

    // 侧边圆角逻辑可自行在此处补充
  }

  /// 绘制轨道下方居中对齐滑块的标签
  void _drawBottomLabel({
    required Canvas canvas,
    required Offset thumbCenter,
    required SliderThemeData sliderTheme,
    required double trackBottom,
  }) {
    // 替换为实际需要显示的标签数值,可根据业务逻辑自定义
    final labelText = sliderTheme.valueIndicatorFormatter?.call(0) ?? "0";
    final textPainter = TextPainter(
      text: TextSpan(
        text: labelText,
        style: labelStyle ?? const TextStyle(fontSize: 12, color: Colors.black87),
      ),
      textDirection: TextDirection.ltr,
    )..layout();

    // 标签坐标:水平居中对齐滑块,垂直位于轨道下方预留间距位置
    final labelPos = Offset(
      thumbCenter.dx - (textPainter.width + labelPadding.width) / 2,
      trackBottom + labelTopGap,
    );

    // 标签背景,不需要可删除
    final labelBg = RRect.fromRectAndRadius(
      Rect.fromLTWH(
        labelPos.dx - labelPadding.width / 2,
        labelPos.dy - labelPadding.height / 2,
        textPainter.width + labelPadding.width,
        textPainter.height + labelPadding.height,
      ),
      const Radius.circular(4),
    );
    canvas.drawRRect(labelBg, Paint()..color = Colors.white);

    textPainter.paint(canvas, labelPos);
  }
}

调用示例

所有尺寸、样式参数都支持在初始化时自定义传入:

// 组件内使用
double _currentValue = 30;

@override
Widget build(BuildContext context) {
  return SliderTheme(
    data: SliderTheme.of(context).copyWith(
      trackShape: const VariableHeightTrackShape(
        minTrackHeight: 4,
        maxTrackHeight: 12,
        labelTopGap: 8,
        labelPadding: Size(10, 4),
        labelStyle: TextStyle(fontSize: 11, color: Colors.grey),
      ),
      // 其他滑块主题配置(滑块颜色、指示器样式等)照常设置即可
    ),
    child: Slider(
      value: _currentValue,
      min: 0,
      max: 100,
      onChanged: (val) => setState(() => _currentValue = val),
    ),
  );
}

内容的提问来源于stack exchange,提问作者Abulkhair

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.05 16:15:46