flutter中使用painter實現(xiàn)數(shù)值調(diào)節(jié)絲滑效果

前言

最近收到了一朋友求助,希望幫忙使用Flutter開發(fā)一個組件,實現(xiàn)特定App上調(diào)節(jié)參數(shù)功能功能組件,要求:
1.扇形動畫調(diào)節(jié)有動畫效果使用起來絲滑順暢
2.有一定的邊界效果
之前開發(fā)過類似圖形,以及自定義繪制相關(guān)的開發(fā),所以難度不是很大,且看下本人實現(xiàn)后最終效果,總的來說朋友很是滿意,在此也貢獻(xiàn)出來希望可以幫有需要的朋友。
代碼地址

horizon.gif
vertical.gif

需求分析

難點(diǎn)分析點(diǎn)線區(qū)域高亮其實這些都是比較簡單,主要的難點(diǎn)終點(diǎn)在哪里,例如橫向起點(diǎn)坐標(biāo)x=0,終點(diǎn)坐標(biāo),縱坐標(biāo)很容易確定就是和起點(diǎn)一樣的y, 不好確定是x;
那么怎么辦呢,仔細(xì)觀察圓弧是落在圓周上的,這就很簡單了利用勾股定理就很容易解決了,思路有了下面就是上代碼了。還有一些就是坐標(biāo)的轉(zhuǎn)化,自適應(yīng)適配,下面列出實現(xiàn)代碼希望可以幫助到需要朋友

代碼實現(xiàn)

實現(xiàn)上來說并不復(fù)雜,大部分代碼都是在處理繪制相關(guān)的工作

typedef ValueChanged = void Function(double value);

class SliderHorizontal extends StatefulWidget {
  /// size 是畫布大小 實際寬度還需要要比這個大 imgWidth
  final double size;
  //取值范圍 0-100
  final double value;
  //可取最小值
  final double minValue;
  //可取最大值
  final double maxValue;
  final ValueChanged? onChanged;
  final SliderController? sliderController;

  const SliderHorizontal(
      {super.key,
      required this.size,
      required this.value,
      required this.minValue,
      required this.maxValue,
      this.onChanged,
      this.sliderController})
      : assert(maxValue > minValue),
        assert(value >= minValue),
        assert(value <= maxValue);

  @override
  State<SliderHorizontal> createState() => _SliderHorizontalState();
}

class _SliderHorizontalState extends State<SliderHorizontal> {
  double get imgWidth {
    return 77.gr;
  }

  double get imgLeft {
    return 15.gr;
  }

  //當(dāng)前所在值 取值范圍0-100
  double currentValue = 0;
  double get imgDx {
    var rate =
        (currentValue - widget.minValue) / (widget.maxValue - widget.minValue);
    return widget.size * 0.2 - 20.gr + rate * (widget.size * 0.6);
    // * currentValue / 100 - 20.gr;
  }

  @override
  void initState() {
    super.initState();
    RatioUtil.init(size: widget.size, designSize: 300);
    currentValue = widget.value;
    widget.sliderController?.addStep = addStep;
    widget.sliderController?.subStep = subStep;
  }

  void addStep() {
    onChanged(currentValue + 0.1);
  }

  void subStep() {
    onChanged(currentValue - 0.1);
  }

  void onChanged(double value) {
    if (value < widget.minValue) {
      value = widget.minValue;
    }
    if (value > widget.maxValue) {
      value = widget.maxValue;
    }
    widget.onChanged?.call(value);
    setState(() {
      currentValue = value;
    });
  }

  @override
  Widget build(BuildContext context) {
    var size = widget.size;
    return SizedBox(
      width: size + imgWidth,
      height: size + imgWidth,
      child: Stack(
        clipBehavior: Clip.none,
        children: [
          Padding(
            padding: EdgeInsets.only(left: imgWidth, top: imgWidth),
            child: CustomPaint(
              size: Size(size, size),
              painter: VerticalPainter(
                  minValue: widget.minValue,
                  maxValue: widget.maxValue,
                  value: currentValue),
            ),
          ),
          Positioned(left: 0, bottom: imgDx, child: _buildImg()),
          Positioned(
            height: 0.6 * size,
            bottom: 0.2 * size,
            left: 0,
            right: 0,
            child: RotatedBox(
              quarterTurns: 3,
              child: _buildSlider(),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildSlider() {
    return BetterCupertinoSlider(
      min: widget.minValue,
      max: widget.maxValue,
      value: currentValue,
      configure: BetterCupertinoSliderConfigure(
        trackHorizontalPadding: 0.gr,
        trackHeight: 20.gr,
        trackLeftColor: Colors.transparent,
        trackRightColor: Colors.transparent,
        thumbRadius: 20.gr,
        thumbPainter: (canvas, rect) {
          final RRect rrect = RRect.fromRectAndRadius(
            rect,
            Radius.circular(rect.shortestSide / 2.0),
          );
          canvas.drawRRect(rrect, Paint()..color = Colors.transparent);
        },
      ),
      onChanged: onChanged,
    );
  }

  Widget _buildImg() {
    return Stack(
      children: [
        Image.asset(
          Assets.images.horizontalHandle.path,
          width: imgWidth,
        ),
        Positioned.fill(
          left: imgLeft,
          right: 0,
          bottom: 0,
          top: 0,
          child: Container(
            alignment: Alignment.center,
            child: Text(currentValue.toStringAsFixed(1),
                style: TextStyle(color: Colors.white, fontSize: 16.gr)),
          ),
        )
      ],
    );
  }
}

class VerticalPainter extends CustomPainter {
  final double maxValue;
  final double minValue;
  final double value;

  VerticalPainter(
      {required this.maxValue, required this.minValue, required this.value});

  TextPainter getTextPainter(String text) {
    return TextPainter(
      text: TextSpan(text: text, style: TextStyle(fontSize: 16.gr)),
      textDirection: TextDirection.rtl,
    )..layout(minWidth: 0, maxWidth: 20);
  }

  @override
  void paint(Canvas canvas, Size size) {
    final double axisLength = size.width;
    final Offset origin = Offset(0, size.height);
    final Paint axisPaint = Paint()
      ..color = Colors.white
      ..strokeWidth = 5.0.gr
      ..style = PaintingStyle.stroke
      ..strokeJoin = StrokeJoin.round
      ..strokeCap = StrokeCap.round;

    final hPainter = getTextPainter('H');
    hPainter.paint(canvas, origin + Offset(-5, -axisLength - 30));

    final qPainter = getTextPainter('Q');
    qPainter.paint(canvas, origin + Offset(axisLength + 10, -10));

    final Path xyPath = Path()
      ..moveTo(0, 0)
      ..lineTo(0, origin.dy)
      ..lineTo(origin.dy, origin.dy);
    canvas.drawPath(xyPath, axisPaint);

    // final zeroPainter = getTextPainter('0');
    // zeroPainter.paint(canvas, Offset(-20, origin.dy - 10));

    //0.2 的位置點(diǎn)
    final realLength = 0.6 * axisLength;
    final minPainter = getTextPainter(minValue.toStringAsFixed(0));
    final minDy = origin.dy - 0.2 * axisLength;
    minPainter.paint(canvas, Offset(-25, minDy - 8.gr));

    final maxPainter = getTextPainter(maxValue.toStringAsFixed(0));
    final maxDy = origin.dy - 0.8 * axisLength;
    maxPainter.paint(canvas, Offset(-25, maxDy - 8.gr));

    // 繪制從X軸到Y(jié)軸的頂點(diǎn)的弧形
    final double arcRadius = axisLength;
    final Rect arcRect = Rect.fromCircle(center: origin, radius: arcRadius);
    final Paint arcPaint = Paint()
      ..color = const Color(0x99FFFFFF)
      ..style = PaintingStyle.stroke
      ..strokeWidth = 1.0;

    canvas.drawArc(arcRect, -pi / 2, pi / 2, false, arcPaint);

    final Path rectPath = Path()
      ..moveTo(calculateB(axisLength, axisLength - minDy), minDy)
      ..lineTo(0, minDy)
      ..lineTo(0, maxDy)
      ..lineTo(calculateB(axisLength, axisLength - maxDy), maxDy)
      ..arcToPoint(Offset(calculateB(axisLength, axisLength - minDy), minDy),
          radius: Radius.circular(axisLength));
    final Paint rectPaint = Paint()
      ..color = const Color(0x61FFFFFF)
      ..style = PaintingStyle.fill;

    canvas.drawPath(rectPath, rectPaint);

    final Paint strokePaint = Paint()
      ..color = Colors.white
      ..strokeWidth = 1
      ..style = PaintingStyle.stroke;

    canvas.drawPath(rectPath, strokePaint);

    //百分比
    final rate = (value - minValue) / (maxValue - minValue);
    final double posY = axisLength - (0.2 * axisLength + rate * realLength);
    final startPoint = Offset(calculateB(axisLength, axisLength - posY), posY);
    final endPoint = Offset(0, posY);

    final Paint soildPaint = Paint()
      ..color = Colors.white
      ..strokeWidth = 4.0.gr;

    final Paint soildPaint1 = Paint()
      ..color = const Color(0x61FFFFFF)
      ..strokeWidth = 12.0.gr;

    // 桿子
    canvas.drawLine(startPoint, endPoint, soildPaint1);
    canvas.drawLine(startPoint, endPoint, soildPaint);

    //圓頭
    canvas.drawOval(
        Rect.fromCircle(center: startPoint, radius: 8.gr), soildPaint);
    canvas.drawOval(
        Rect.fromCircle(center: startPoint, radius: 12.gr), soildPaint1);
  }

  double calculateB(double c, double a) {
    return sqrt(c * c - a * a);
  }

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。