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

如何在Flutter中实现这种曲线形状的Slider?

Flutter 实现曲线形状滑块(Slider)

要实现自定义曲线外观的滑块,核心思路是通过 CustomPaint 绘制曲线轨道,并手动处理滑块的位置计算与拖动逻辑,以下是具体实现方案:

实现步骤与代码示例

1. 自定义曲线滑块组件

通过 StatefulWidget 封装逻辑,结合 CustomPaint 绘制曲线轨道,同时处理触摸拖动时的数值映射:

import 'package:flutter/material.dart';

class CurvedSlider extends StatefulWidget {
  final double value;
  final ValueChanged<double> onChanged;
  final double min;
  final double max;

  const CurvedSlider({
    super.key,
    required this.value,
    required this.onChanged,
    this.min = 0.0,
    this.max = 1.0,
  });

  @override
  State<CurvedSlider> createState() => _CurvedSliderState();
}

class _CurvedSliderState extends State<CurvedSlider> {
  late double _currentValue;

  @override
  void initState() {
    super.initState();
    _currentValue = widget.value;
  }

  @override
  void didUpdateWidget(CurvedSlider oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.value != widget.value) {
      _currentValue = widget.value;
    }
  }

  // 根据value计算曲线上的对应点(示例用二次贝塞尔曲线)
  Offset _getCurvePoint(double value) {
    final normalizedValue = (value - widget.min) / (widget.max - widget.min);
    final width = MediaQuery.of(context).size.width * 0.8;
    final height = 100.0;
    // 定义曲线起点、控制点、终点
    final start = Offset(0, height / 2);
    final control = Offset(width / 2, height);
    final end = Offset(width, height / 2);
    // 计算贝塞尔曲线上的点
    return Offset.lerp(start, control, normalizedValue)! +
        Offset.lerp(control, end, normalizedValue)! -
        Offset.lerp(start, end, normalizedValue)!;
  }

  // 将触摸坐标转换为对应的滑块value(简化版,可根据需求优化精度)
  double _getTouchValue(Offset globalPosition) {
    final renderBox = context.findRenderObject() as RenderBox;
    final localPosition = renderBox.globalToLocal(globalPosition);
    final normalizedX = localPosition.dx / renderBox.size.width;
    return widget.min + normalizedX * (widget.max - widget.min);
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onPanUpdate: (details) {
        final value = _getTouchValue(details.globalPosition);
        _currentValue = value.clamp(widget.min, widget.max);
        widget.onChanged(_currentValue);
      },
      child: CustomPaint(
        size: Size(MediaQuery.of(context).size.width * 0.8, 100),
        painter: _CurvedSliderPainter(
          currentValue: _currentValue,
          min: widget.min,
          max: widget.max,
          getCurvePoint: _getCurvePoint,
        ),
      ),
    );
  }
}

// 负责绘制曲线轨道、选中区域和滑块的Painter
class _CurvedSliderPainter extends CustomPainter {
  final double currentValue;
  final double min;
  final double max;
  final Offset Function(double) getCurvePoint;

  _CurvedSliderPainter({
    required this.currentValue,
    required this.min,
    required this.max,
    required this.getCurvePoint,
  });

  @override
  void paint(Canvas canvas, Size size) {
    // 绘制未选中的曲线轨道
    final trackPaint = Paint()
      ..color = Colors.grey[300]!
      ..strokeWidth = 4
      ..style = PaintingStyle.stroke;
    final trackPath = Path();
    trackPath.moveTo(0, size.height / 2);
    trackPath.quadraticBezierTo(
      size.width / 2,
      size.height,
      size.width,
      size.height / 2,
    );
    canvas.drawPath(trackPath, trackPaint);

    // 绘制已选中的曲线部分
    final normalizedValue = (currentValue - min) / (max - min);
    final selectedPaint = Paint()
      ..color = Colors.blue
      ..strokeWidth = 4
      ..style = PaintingStyle.stroke;
    final selectedPath = Path();
    selectedPath.moveTo(0, size.height / 2);
    selectedPath.quadraticBezierTo(
      size.width / 2 * normalizedValue * 2,
      size.height * normalizedValue,
      getCurvePoint(currentValue).dx,
      getCurvePoint(currentValue).dy,
    );
    canvas.drawPath(selectedPath, selectedPaint);

    // 绘制滑块
    final thumbPaint = Paint()..color = Colors.blue;
    final thumbCenter = getCurvePoint(currentValue);
    canvas.drawCircle(thumbCenter, 12, thumbPaint);
  }

  @override
  bool shouldRepaint(covariant _CurvedSliderPainter oldDelegate) {
    return oldDelegate.currentValue != currentValue;
  }
}

2. 使用示例

将自定义滑块整合到页面中:

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

  @override
  State<CurvedSliderDemo> createState() => _CurvedSliderDemoState();
}

class _CurvedSliderDemoState extends State<CurvedSliderDemo> {
  double _value = 0.5;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('曲线滑块示例')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            CurvedSlider(
              value: _value,
              onChanged: (newValue) => setState(() => _value = newValue),
            ),
            const SizedBox(height: 20),
            Text('当前值: ${_value.toStringAsFixed(2)}'),
          ],
        ),
      ),
    );
  }
}

自定义调整要点

  • 曲线形状:修改 _getCurvePoint 中的曲线参数(如替换三次贝塞尔曲线、自定义路径),可实现任意曲线外观
  • 样式定制:调整 _CurvedSliderPainter 中的画笔颜色、宽度、滑块大小,匹配你的UI设计
  • 精度优化:如果需要更精准的触摸映射,可以通过计算触摸点到曲线的最近点,替换当前简化的_getTouchValue逻辑

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.06 07:35:11