Flutter 自定义Slider

855 阅读2分钟

 

主要解决系统拖条不便于自定义样式,和排版奇怪的问题

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


class CustomSlider extends StatelessWidget {
  /// 进度条的粗细
  final double width;
  
  /// 输入输出的值的范围 [0, 1]
  final double value;
  final void Function(double value, bool isEnd) valueChanged;

  /// 自定义样式的方法
  final Widget Function(BuildContext context, Size size)? activeBuilder;
  final Widget Function(BuildContext context, Size size)? inactiveBuilder;
  final Widget Function(BuildContext context)? sliderBarBuilder;

  const CustomSlider({
    required this.value,
    required this.valueChanged,
    super.key,
    this.width = 10,
    this.sliderBarBuilder,
    this.activeBuilder,
    this.inactiveBuilder,
  });

  @override
  Widget build(BuildContext context) {
    late Widget sliderBar;
    if (sliderBarBuilder != null) {
      sliderBar = sliderBarBuilder!(context);
    } else {
      sliderBar = Container(
        width: 24,
        height: 24,
        decoration: const ShapeDecoration(
          color: Colors.white,
          shape: OvalBorder(
            side: BorderSide(width: 1, color: Color(0xFF954CFF)),
          ),
          shadows: [
            BoxShadow(
              color: Color(0x66AAA6AA),
              blurRadius: 2,
              offset: Offset(1, 1),
              spreadRadius: 0,
            )
          ],
        ),
      );
    }

    return LayoutBuilder(builder: (BuildContext context, constraints) {
      double maxWidth = constraints.maxWidth;
      double sliderWidth = maxWidth;
      double sliderHeight = width;
      late Widget inactive;
      if (inactiveBuilder != null) {
        inactive = inactiveBuilder!(context, Size(sliderWidth, sliderHeight));
      } else {
        inactive = Container(
          width: sliderWidth,
          height: sliderHeight,
          decoration: ShapeDecoration(
            color: const Color(0xFFF3F2F4),
            shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(100),
            ),
          ),
        );
      }

      double activeWidth = sliderWidth * value;
      double activeHeight = sliderHeight;
      late Widget active;
      if (activeBuilder != null) {
        active = activeBuilder!(context, Size(activeWidth, activeHeight));
      } else {
        active = Container(
          width: activeWidth,
          height: activeHeight,
          decoration: ShapeDecoration(
            color: const Color(0xFF954CFF),
            shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(100),
            ),
          ),
        );
      }

      return GestureDetector(
        child: Stack(
          alignment: AlignmentDirectional.centerStart,
          children: [
            inactive,
            active,
            Align(
              alignment: FractionalOffset(value, 0.5),
              child: sliderBar,
            ),
          ],
        ),
        onTapDown: (details) {
          updateDx(details.localPosition, maxWidth);
        },
        onTapUp: (details) {
          updateDx(details.localPosition, maxWidth, isEnd: true);
        },
        onPanUpdate: (details) {
          updateDx(details.localPosition, maxWidth);
        },
      );
    });
  }

  void updateDx(Offset value, double maxX, {bool isEnd = false}) {
    double dx = value.dx;
    dx = max(0, dx);
    dx = min(maxX, dx);
    valueChanged(dx / maxX, isEnd);
  }
}

旧版的一些方法,没有彻底解决自定义的一些需求,但是使用画笔绘制性能会好些,对于简单的需求可以参考以下实现。

enum SliderDirection {
  vertical,
  horizontal
}

class SliderBar extends StatelessWidget {

  final Size size;
  final Widget child;

  SliderBar({this.child, this.size = const Size(24, 24)});

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: size.width,
      height: size.height,
      child: child ?? thumb(),
    );
  }

  Widget thumb() {
    return Container(width: size.width, height: size.height,).backgroundColor(color: Colors.white).cornerRadius(radius: min(size.width, size.height));
  }

}

// ignore: must_be_immutable
class CustomSlider extends StatelessWidget {

  //UI
  //不论竖着还是横着,width是短边,height是长边,短边默认值20,长边撑满
  final double width;
  final double height;

  final Color  activeTrackColor;

  final SliderBar sliderBar;

  final double value;
  final Function(double, bool) valueChanged;

  final SliderDirection direction;
  final Decoration background;

  CustomSlider(this.value, this.valueChanged, {
    this.width = 24,
    this.height = double.infinity,
    this.direction = SliderDirection.horizontal,
    this.background,
    this.activeTrackColor,
    this.sliderBar
  });

  double dx = 0;
  double maxX = 0;

  bool get isVertical => direction == SliderDirection.vertical;

  @override
  Widget build(BuildContext context) {
    Decoration decoration = this.background ?? BoxDecoration(
        color: Colors.grey[300]
    );

    return GestureDetector(
      child: Stack(
        alignment: AlignmentDirectional.center,
        children: [
          Container(
            height: isVertical ? height : width,
            width: isVertical ? width : height,
            child: CustomPaint(
              painter: SliderPainter(
                (double maxDx) {
                  maxX = maxDx;
                  return value * maxDx;
                },
                vertical: isVertical,
                activeTrackColor: activeTrackColor,
              ),
            ),
            decoration: decoration
          ),
          Align(child: sliderBar, alignment: FractionalOffset(isVertical ? 0.5 : value,isVertical ? 1 - value : 0.5)),
        ],
      ).size(width: isVertical ? max(sliderBar.size.width, width) : height, height: isVertical ? height : null),
      onTapDown: (details) {
        updateDx(getPoint(context, details.globalPosition));
      },
      onTapUp: (details) {
        setValue(true);
      },
      onPanUpdate: (details){
        updateDx(getPoint(context, details.globalPosition));
      },
      onPanEnd: (details){
        setValue(true);
      },
    );
  }

  Offset getPoint(BuildContext context, Offset globalPosition) {
    RenderBox renderBox = context.findRenderObject();
    return renderBox.globalToLocal(globalPosition);
  }

  void updateDx(Offset value) {

    dx = isVertical ? value.dy : value.dx;

    dx = dx < 0 ? 0 : dx;
    dx = dx > maxX ? maxX : dx;

    setValue(false);
  }

  void setValue(bool isEnd) {
    valueChanged(isVertical ? ((maxX - dx) / maxX) : dx/maxX, isEnd);
  }
}

class SliderPainter extends CustomPainter {

  final Color  activeTrackColor;

  final double Function(double maxDx) getDx;
  final bool   vertical;

  SliderPainter(this.getDx, {
    this.activeTrackColor,
    this.vertical = false,
  });

  /// 初始化画笔
  var lineP = Paint()
    ..strokeCap = StrokeCap.butt;

  var thumbP = Paint()
    ..strokeCap = StrokeCap.round;

  @override
  void paint(Canvas canvas, Size size) {

    double width = vertical ? size.width : size.height;
    double height = vertical ? size.height : size.width;

    lineP.strokeWidth = width;
    lineP.color = this.activeTrackColor ?? Colors.blue;

    double dx = getDx(height);
    Offset endPoint = Offset.zero;

    double centerW = width / 2;

    /// 通过canvas画线
    if (vertical == true) {
      endPoint = Offset(centerW, height - dx);
      canvas.drawLine(Offset(centerW, height), endPoint , lineP);
    } else {
      endPoint = Offset(dx, centerW);
      canvas.drawLine(Offset(0, centerW), endPoint , lineP);
    }
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return true;
  }

}