Flutter 胶囊滑条 Slider

935 阅读2分钟

PixPin_2025-05-21_16-42-10.gif

一个用来实现胶囊滑条的代码 使用方法

import 'package:flutter/material.dart';
import 'package:permission_handler_test/capsule_silder/capsule_slider.dart';

void main() {
  runApp(const MaterialApp(
      home: CapsuleSliderTest()));
}

class CapsuleSliderTest extends StatefulWidget {
  const CapsuleSliderTest({super.key});

  @override
  State<CapsuleSliderTest> createState() => _CapsuleSliderTestState();
}

class _CapsuleSliderTestState extends State<CapsuleSliderTest> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Padding(
        padding: const EdgeInsets.all(28.0),
        child: Column(
          children: [
            const SizedBox(height: 100,),
            CapsuleSlider(value: 1, min: 0, max: 100,
              onChanged: (v){
            },)
          ],
        ),
      ),
    );
  }
}

详细注释在代码中 若有其他问题可以评论区或私信

import 'dart:math' as math;
import 'package:flutter/material.dart';

/// 胶囊形拖动条(纯手势实现,不依赖 Slider)
/// ---------------------------------------------------------------------------
/// * value           当前数值
/// * min / max       取值范围
/// * onChanged       拖动过程中实时回调
/// * onChangeEnd     抬手回调
/// * move            是否处于“可调节”状态,影响轨道颜色 也可以放在禁用手势,此处只是用作颜色区分
/// * height          轨道高度(= 圆角直径)
/// * activeColor     move==true 时已选轨道颜色
/// * noMoveColor     move==false 时已选轨道颜色
/// * inactiveColor   未选轨道颜色
/// * showThumb       是否显示小圆手柄
/// ---------------------------------------------------------------------------
class CapsuleSlider extends StatefulWidget {
  const CapsuleSlider({
    super.key,
    required this.value,
    required this.min,
    required this.max,
    required this.onChanged,
    this.move = false,
    this.onChangeEnd,
    this.height = 46,
    this.activeColor = const Color(0xfffa0200),
    this.inactiveColor = const Color(0xffF3F4F9),
    this.noMoveColor = const Color(0xffD6D6D6),
    this.showThumb = false,
    this.thumbRadius = 10,
    this.thumbColor = Colors.white,
  });

  // ---- 对外属性 ----
  final double value;
  final double min;
  final double max;

  final bool move;
  final ValueChanged<double> onChanged;
  final ValueChanged<double>? onChangeEnd;

  final double height;          // 轨道高度
  final Color activeColor;      // 可调节时的已选颜色
  final Color inactiveColor;    // 背景颜色
  final Color noMoveColor;      // 不可调节时的已选颜色

  final bool showThumb;
  final double thumbRadius;
  final Color thumbColor;

  @override
  State<CapsuleSlider> createState() => _CapsuleSliderState();
}

class _CapsuleSliderState extends State<CapsuleSlider> {
  late double _value;   // 内部持有当前值,实时刷新
  late double _width;   // 组件宽度,用于像素↔百分比换算

  @override
  void initState() {
    super.initState();
    _value = widget.value;      // 初始化
  }

  /// 当父组件外部更新 value 时,同步内部 _value
  @override
  void didUpdateWidget(covariant CapsuleSlider old) {
    super.didUpdateWidget(old);
    if (old.value != widget.value) _value = widget.value;
  }

  /// 处理一次拖动 / 点击,localPos 为手指在组件内的坐标
  void _handleDrag(Offset localPos) {
    // 把 X 坐标映射到 0~1 百分比
    final pct = (localPos.dx / _width).clamp(0.0, 1.0);
    // 再映射到实际 min~max 区间
    final newVal = widget.min + pct * (widget.max - widget.min);

    if (newVal != _value) {
      setState(() => _value = newVal); // 刷新 UI
      widget.onChanged(newVal);        // 通知外部
    }
  }

  @override
  Widget build(BuildContext context) {
    final r = widget.height / 2;                 // 圆角半径
    final pct = (_value - widget.min) / (widget.max - widget.min);

    return LayoutBuilder(
      builder: (_, constraints) {
        _width = constraints.maxWidth;           // 得到真实宽度
        final activeW = _width * pct;            // 已选轨道宽度

        return GestureDetector(
          behavior: HitTestBehavior.translucent, // 空白处也能响应
          // —— 手势回调:点击 / 拖动 / 抬手 ——
          onPanDown:  (d) => _handleDrag(d.localPosition),
          onPanUpdate:(d) => _handleDrag(d.localPosition),
          onPanEnd:   (_) => widget.onChangeEnd?.call(_value),
          onTapDown:  (d) => _handleDrag(d.localPosition),

          // —— 裁剪为胶囊形,防止已选轨道溢出 ——
          child: ClipRRect(
            borderRadius: BorderRadius.circular(r),

            child: Stack(
              alignment: Alignment.centerLeft,
              children: [
                // 背景轨道(未选部分)
                Container(
                  height: widget.height,
                  decoration: BoxDecoration(
                    color: widget.inactiveColor,
                    borderRadius: BorderRadius.circular(r),
                  ),
                ),
                // 已选轨道:颜色取决于 move
                Container(
                  height: widget.height,
                  width: activeW,
                  decoration: BoxDecoration(
                    color: widget.move
                        ? widget.activeColor
                        : widget.noMoveColor,
                    borderRadius: BorderRadius.circular(r),
                  ),
                ),
                // 可选:手柄
                if (widget.showThumb)
                  Positioned(
                    // 让手柄中心对齐已选轨道右端
                    left: math.max(0, activeW - widget.thumbRadius),
                    child: Container(
                      width: widget.thumbRadius * 2,
                      height: widget.thumbRadius * 2,
                      decoration: BoxDecoration(
                        color: widget.thumbColor,
                        shape: BoxShape.circle,
                        boxShadow: [
                          // 简单投影,让手柄有悬浮感
                          BoxShadow(
                            color: Colors.black26,
                            blurRadius: 3,
                            offset: const Offset(0, 1),
                          )
                        ],
                      ),
                    ),
                  ),
              ],
            ),
          ),
        );
      },
    );
  }
}