如何在我的小部件周围创建动画(圆形)矩形边框? [英] How can I create an animated (rounded) rectangular border around my widget?

查看:17
本文介绍了如何在我的小部件周围创建动画(圆形)矩形边框?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

如何将小部件的边框从 0% 设置为 100%(类似于在 Adob​​e AfterEffects 中创建的修剪路径效果)?我想将此应用于具有矩形或圆角矩形形状的小部件.

How can animate the border of a widget from 0 to 100% (similar to the Trim Path effects one can create in Adobe AfterEffects)? I want to apply this to widgets that have a rectangular or rounded rectangle shape.

这是我想要达到的效果的一个例子:

Here's an example of the effect I am trying to achieve:

推荐答案

大局:

让我们将小部件包装在 CustomPaint 中.由于 CustomPaint 采用其子项的大小,因此我们不必担心在正确的位置进行绘制.

The big picture:

Let's wrap the widget in a CustomPaint. Since the CustomPaint takes its child's size, we don't have to worry about painting at the correct position.

我们可以进一步将这个关于通用路径动画的精彩答案作为起点,并调整代码,以便我们的AnimatedBorderPainter可以为矩形、圆角矩形和圆形绘制路径.

We can further take this wonderful answer for generic path animations as a starting point and tweak the code so that our AnimatedBorderPainter can paint paths for rectangles, rounded rectangles and circles.

最后,我们创建一个 AnimationController 并定义我们需要的持续时间、曲线和所有其他属性.

Finally, we create an AnimationController and define the duration, curve and all other properties that we need.

AnimatedBorderPainterpaint方法中,我们首先在动画开始时创建_originalPath(即完整路径),然后在后续(重新)根据动画的进度绘制 currentPath._createAnimatedPath 方法取自上述答案,其中对其进行了更详细的描述.

In the paint method of the AnimatedBorderPainter we first create the _originalPath (i.e. the complete path) when the animation starts and then subsequently (re)draw the currentPath based on the animation's progress. The _createAnimatedPath method is taken from the above-mentioned answer where it is described in greater detail.

  late Path _originalPath;
  late Paint _paint;

  @override
  void paint(Canvas canvas, Size size) {
    final animationPercent = _animation.value;

    // Construct original path once when animation starts
    if (animationPercent == 0.0) {
      _originalPath = _createOriginalPath(size);
      _paint = Paint()
        ..strokeWidth = _strokeWidth
        ..style = PaintingStyle.stroke
        ..color = _strokeColor;
    }

    final currentPath = _createAnimatedPath(
      _originalPath,
      animationPercent,
    );

    canvas.drawPath(currentPath, _paint);
  }

让我们专注于为我们的形状创建原始路径.我们可以使用addRect(矩形)、addRRect(圆角矩形)和addOval(圆形)来创建各自的形状:

Let's focus on creating the original path for our shapes. We can use addRect (rectangle), addRRect (rounded rectangle) and addOval (circle) to create the respective shapes:

Path _createOriginalPath(Size size) {
    switch (_pathType) {
      case PathType.rect:
        return _createOriginalPathRect(size);
      case PathType.rRect:
        return _createOriginalPathRRect(size);
      case PathType.circle:
        return _createOriginalPathCircle(size);
    }
  }

  Path _createOriginalPathRect(Size size) {
    Path originalPath = Path()
      ..addRect(
        Rect.fromLTWH(0, 0, size.width, size.height),
      )
      ..lineTo(0, -(_strokeWidth / 2));
    if (_startingPercentage > 0 && _startingPercentage < 100) {
      return _createPathForStartingPercentage(
          originalPath, PathType.rect, size);
    }
    return originalPath;
  }

  Path _createOriginalPathRRect(Size size) {
    Path originalPath = Path()
      ..addRRect(
        RRect.fromRectAndRadius(
          Rect.fromLTWH(0, 0, size.width, size.height),
          _radius,
        ),
      );
    if (_startingPercentage > 0 && _startingPercentage < 100) {
      return _createPathForStartingPercentage(originalPath, PathType.rRect);
    }
    return originalPath;
  }

  Path _createOriginalPathCircle(Size size) {
    Path originalPath = Path()
      ..addOval(
        Rect.fromLTWH(0, 0, size.width, size.height),
      );
    if (_startingPercentage > 0 && _startingPercentage < 100) {
      return _createPathForStartingPercentage(originalPath, PathType.circle);
    }
    return originalPath;
  }

由于我们还想定义路径动画的开始位置(使用 startingPercentage 参数),我们必须根据输入剪切并重新加入我们最初构建的路径:

Since we also want to define where our path animation starts (using the startingPercentage parameter), we have to cut and rejoin our originally constructed path based on the input:

  Path _createPathForStartingPercentage(Path originalPath, PathType pathType,
      [Size? size]) {
    // Assumes that original path consists of one subpath only
    final pathMetrics = originalPath.computeMetrics().first;
    final pathCutoffPoint = (_startingPercentage / 100) * pathMetrics.length;
    final firstSubPath = pathMetrics.extractPath(0, pathCutoffPoint);
    final secondSubPath =
        pathMetrics.extractPath(pathCutoffPoint, pathMetrics.length);
    if (pathType == PathType.rect) {
      Path path = Path()
        ..addPath(secondSubPath, Offset.zero)
        ..lineTo(0, -(_strokeWidth / 2))
        ..addPath(firstSubPath, Offset.zero);
      switch (_startingPercentage) {
        case 25:
          path.lineTo(size!.width + _strokeWidth / 2, 0);
          break;
        case 50:
          path.lineTo(size!.width - _strokeWidth / 2, size.height);
          break;
        case 75:
          path.lineTo(0, size!.height + _strokeWidth / 2);
          break;
        default:
      }
      return path;
    }
    return Path()
      ..addPath(secondSubPath, Offset.zero)
      ..addPath(firstSubPath, Offset.zero);
  }

虽然还有更多内容(参见下面的完整代码),但我们基本上可以如下使用我们的 AnimatedBorderPainter,定义诸如 startingPercentage 之类的东西,animationDirectionradius(后者仅与圆角矩形相关):

Although there is still a little more to it (see full code below), we can then basically use our AnimatedBorderPainter as follows, defining things like the startingPercentage, the animationDirection and the radius (the latter being only relevant for rounded rectangles):

          CustomPaint(
            foregroundPainter: AnimatedBorderPainter(
              animation: _controller1,
              strokeColor: Colors.black,
              pathType: PathType.rRect,
              animationDirection: AnimationDirection.clockwise,
              startingPercentage: 40,
              radius: const Radius.circular(12),
            ),
            child: ElevatedButton(
              child: const Text('Click me also!'),
              onPressed: _startAnimation1,
              style: ElevatedButton.styleFrom(
                shape: RoundedRectangleBorder(
                  borderRadius: BorderRadius.circular(12),
                ),
              ),
            ),
          ),

完整的代码,包括您可以在 DartPad 中运行的示例动画:

Full code including example animations you can run in DartPad:

import 'dart:ui';

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Border Animation',
        home: Scaffold(body: ExampleAnimatedBorderPainter()));
  }
}

// Example code including two animations
class ExampleAnimatedBorderPainter extends StatefulWidget {
  @override
  State<ExampleAnimatedBorderPainter> createState() =>
      _ExampleAnimatedBorderPainterState();
}

class _ExampleAnimatedBorderPainterState
    extends State<ExampleAnimatedBorderPainter> with TickerProviderStateMixin {
  late AnimationController _controller1;
  late AnimationController _controller2;

  @override
  void initState() {
    super.initState();
    _controller1 = AnimationController(
      vsync: this,
      duration: const Duration(
        milliseconds: 2000,
      ),
    );
    _controller2 = AnimationController(
      vsync: this,
      duration: const Duration(
        milliseconds: 1500,
      ),
    );
  }

  @override
  void dispose() {
    _controller1.dispose();
    _controller2.dispose();
    super.dispose();
  }

  void _startAnimation1() {
    _controller1.reset();
    _controller1.animateTo(1.0, curve: Curves.easeInOut);
  }

  void _startAnimation2() {
    _controller2.reset();
    _controller2.animateTo(1.0, curve: Curves.easeInOut);
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          CustomPaint(
            foregroundPainter: AnimatedBorderPainter(
              animation: _controller1,
              strokeColor: Colors.black,
              pathType: PathType.rRect,
              animationDirection: AnimationDirection.clockwise,
              startingPercentage: 40,
              radius: const Radius.circular(12),
            ),
            child: ElevatedButton(
              child: const Text('Click me also!'),
              onPressed: _startAnimation1,
              style: ElevatedButton.styleFrom(
                shape: RoundedRectangleBorder(
                  borderRadius: BorderRadius.circular(12),
                ),
              ),
            ),
          ),
          const SizedBox(
            height: 20,
          ),
          CustomPaint(
            foregroundPainter: AnimatedBorderPainter(
              animation: _controller2,
              strokeColor: Colors.deepOrange,
              pathType: PathType.rRect,
              animationDirection: AnimationDirection.counterclockwise,
            ),
            child: ElevatedButton(
              child: const Text('Click me also!'),
              onPressed: _startAnimation2,
            ),
          ),
        ],
      ),
    );
  }
}

class AnimatedBorderPainter extends CustomPainter {
  final Animation<double> _animation;
  final PathType _pathType;
  final double _strokeWidth;
  final Color _strokeColor;
  final Radius _radius;
  final int _startingPercentage;
  final AnimationDirection _animationDirection;

  AnimatedBorderPainter({
    required animation,
    PathType pathType = PathType.rect,
    double strokeWidth = 2.0,
    Color strokeColor = Colors.blueGrey,
    Radius radius = const Radius.circular(4.0),
    int startingPercentage = 0,
    AnimationDirection animationDirection = AnimationDirection.clockwise,
  })  : assert(strokeWidth > 0, 'strokeWidth must be greater than 0.'),
        assert(startingPercentage >= 0 && startingPercentage <= 100,
            'startingPercentage must lie between 0 and 100.'),
        _animation = animation,
        _pathType = pathType,
        _strokeWidth = strokeWidth,
        _strokeColor = strokeColor,
        _radius = radius,
        _startingPercentage = startingPercentage,
        _animationDirection = animationDirection,
        super(repaint: animation);

  late Path _originalPath;
  late Paint _paint;

  @override
  void paint(Canvas canvas, Size size) {
    final animationPercent = _animation.value;

    // Construct original path once when animation starts
    if (animationPercent == 0.0) {
      _originalPath = _createOriginalPath(size);
      _paint = Paint()
        ..strokeWidth = _strokeWidth
        ..style = PaintingStyle.stroke
        ..color = _strokeColor;
    }

    final currentPath = _createAnimatedPath(
      _originalPath,
      animationPercent,
    );

    canvas.drawPath(currentPath, _paint);
  }

  @override
  bool shouldRepaint(AnimatedBorderPainter oldDelegate) => true;

  Path _createOriginalPath(Size size) {
    switch (_pathType) {
      case PathType.rect:
        return _createOriginalPathRect(size);
      case PathType.rRect:
        return _createOriginalPathRRect(size);
      case PathType.circle:
        return _createOriginalPathCircle(size);
    }
  }

  Path _createOriginalPathRect(Size size) {
    Path originalPath = Path()
      ..addRect(
        Rect.fromLTWH(0, 0, size.width, size.height),
      )
      ..lineTo(0, -(_strokeWidth / 2));
    if (_startingPercentage > 0 && _startingPercentage < 100) {
      return _createPathForStartingPercentage(
          originalPath, PathType.rect, size);
    }
    return originalPath;
  }

  Path _createOriginalPathRRect(Size size) {
    Path originalPath = Path()
      ..addRRect(
        RRect.fromRectAndRadius(
          Rect.fromLTWH(0, 0, size.width, size.height),
          _radius,
        ),
      );
    if (_startingPercentage > 0 && _startingPercentage < 100) {
      return _createPathForStartingPercentage(originalPath, PathType.rRect);
    }
    return originalPath;
  }

  Path _createOriginalPathCircle(Size size) {
    Path originalPath = Path()
      ..addOval(
        Rect.fromLTWH(0, 0, size.width, size.height),
      );
    if (_startingPercentage > 0 && _startingPercentage < 100) {
      return _createPathForStartingPercentage(originalPath, PathType.circle);
    }
    return originalPath;
  }

  Path _createPathForStartingPercentage(Path originalPath, PathType pathType,
      [Size? size]) {
    // Assumes that original path consists of one subpath only
    final pathMetrics = originalPath.computeMetrics().first;
    final pathCutoffPoint = (_startingPercentage / 100) * pathMetrics.length;
    final firstSubPath = pathMetrics.extractPath(0, pathCutoffPoint);
    final secondSubPath =
        pathMetrics.extractPath(pathCutoffPoint, pathMetrics.length);
    if (pathType == PathType.rect) {
      Path path = Path()
        ..addPath(secondSubPath, Offset.zero)
        ..lineTo(0, -(_strokeWidth / 2))
        ..addPath(firstSubPath, Offset.zero);
      switch (_startingPercentage) {
        case 25:
          path.lineTo(size!.width + _strokeWidth / 2, 0);
          break;
        case 50:
          path.lineTo(size!.width - _strokeWidth / 2, size.height);
          break;
        case 75:
          path.lineTo(0, size!.height + _strokeWidth / 2);
          break;
        default:
      }
      return path;
    }
    return Path()
      ..addPath(secondSubPath, Offset.zero)
      ..addPath(firstSubPath, Offset.zero);
  }

  Path _createAnimatedPath(
    Path originalPath,
    double animationPercent,
  ) {
    // ComputeMetrics can only be iterated once!
    final totalLength = originalPath
        .computeMetrics()
        .fold(0.0, (double prev, PathMetric metric) => prev + metric.length);

    final currentLength = totalLength * animationPercent;

    return _extractPathUntilLength(originalPath, currentLength);
  }

  Path _extractPathUntilLength(
    Path originalPath,
    double length,
  ) {
    var currentLength = 0.0;

    final path = Path();

    var metricsIterator = _animationDirection == AnimationDirection.clockwise
        ? originalPath.computeMetrics().iterator
        : originalPath.computeMetrics().toList().reversed.iterator;

    while (metricsIterator.moveNext()) {
      var metric = metricsIterator.current;

      var nextLength = currentLength + metric.length;

      final isLastSegment = nextLength > length;
      if (isLastSegment) {
        final remainingLength = length - currentLength;
        final pathSegment = _animationDirection == AnimationDirection.clockwise
            ? metric.extractPath(0.0, remainingLength)
            : metric.extractPath(
                metric.length - remainingLength, metric.length);

        path.addPath(pathSegment, Offset.zero);
        break;
      } else {
        // There might be a more efficient way of extracting an entire path
        final pathSegment = metric.extractPath(0.0, metric.length);
        path.addPath(pathSegment, Offset.zero);
      }

      currentLength = nextLength;
    }

    return path;
  }
}

enum PathType {
  rect,
  rRect,
  circle,
}

enum AnimationDirection {
  clockwise,
  counterclockwise,
}

这篇关于如何在我的小部件周围创建动画(圆形)矩形边框?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆