使用重绘为CustomPainter动画,同时还可以在Flutter中检索Controller.value [英] Animating CustomPainter with repaint whilst also retrieving Controller.value in flutter

查看:474
本文介绍了使用重绘为CustomPainter动画,同时还可以在Flutter中检索Controller.value的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我一直在环顾四周,看来给CustomPainter设置动画最有效且资源友好的方法是将其传递给AnimationController作为可重绘的可监听对象,而不是使用SetState().我已经尝试过这种方法,但是不幸的是,我的CustomPaint对象需要一个变量,该变量既可以由controller.value设置,也可以由拖动期间用户指针的位置设置.

I've been looking around and it seems the most efficient and resource friendly way of animating a CustomPainter is to pass it an AnimationController as a repaint listenable, rather than using SetState(). I've tried this method, but unfortunately my CustomPaint object requires a variable that can be set both by the controller.value but also by the position of a user's pointer during a drag.

class CustomScroller extends StatefulWidget {
  final double width; //Should be width of user's screen
  CustomScroller(this.width);
  @override
  _CustomScrollerState createState() => _CustomScrollerState();
}

class _CustomScrollerState extends State<CustomScroller>
    with SingleTickerProviderStateMixin {
  ui.Image _img;
  ui.Picture picture;
  double height = 50;
  AnimationController _controller;
  Animation _animation;
  double dx = 0;
  Offset velocity = Offset(0, 0);
  double currentLocation = 0; //Variable that is required by NewScrollerPainter
  bool loading = true;
  List<PaintTick> upperTicks = [
    PaintTick(100, 4), //1cm,
    PaintTick(1000, 8), //10cm
    PaintTick(5000, 12), //50cm
    PaintTick(10000, 16) //1m
  ];
  List<PaintTick> lowerTicks = [
    PaintTick(254, 5), //1 inch
    PaintTick(3048, 10), //1 foot
  ];

  /// Calculates and runs a [SpringSimulation].
  void _runAnimation(Offset pixelsPerSecond, Size size) {
    _animation = _controller.drive(
      Tween(
        begin: currentLocation,
        end: currentLocation - velocity.dx / 5,
      ),
    );

    // Calculate the velocity relative to the unit interval, [0,1],
    // used by the animation controller.
    final unitsPerSecondX = pixelsPerSecond.dx / size.width;
    final unitsPerSecondY = pixelsPerSecond.dy / size.height;
    final unitsPerSecond = Offset(unitsPerSecondX, unitsPerSecondY);
    final unitVelocity = unitsPerSecond.distance;

    const spring = SpringDescription(
      mass: 30,
      stiffness: 1,
      damping: 0.5,
    );

    final simulation = SpringSimulation(spring, 0, 1, -unitVelocity);

    _controller.animateWith(simulation);
  }

  _loadImage() async {
    ui.PictureRecorder recorder = ui.PictureRecorder();
    Canvas canvas = Canvas(recorder);

    if (lowerTicks == null) {
      lowerTicks = upperTicks;
    }
    double lineHeight = 0;

    Color lineColor = Colors.blueGrey[300];

    Paint paint = Paint()
      ..style = PaintingStyle.stroke
      ..strokeWidth = 1.2
      ..color = lineColor
      ..strokeCap = StrokeCap.round;

    for (int i = 1.toInt(); i < 10001.toInt(); i++) {
      if (upperTicks != null) {
        for (int j = 0; j < upperTicks.length; j++) {
          if ((i).remainder(upperTicks[j].getPosition()) == 0) {
            lineHeight = (upperTicks[j].getHeight());
          }
        }
        //Position to draw
        if (i.remainder(upperTicks[0].getPosition()) == 0) {
          //Draw a meters tick
          canvas.drawLine(Offset(widget.width * (i) / 10000, 0),
              Offset(widget.width * (i) / 10000, lineHeight), paint);
        }
      }
      if (lowerTicks != null) {
        for (int j = 0; j < lowerTicks.length; j++) {
          if ((i).remainder(lowerTicks[j].getPosition()) == 0) {
            lineHeight = (lowerTicks[j].getHeight());
          }
        }

        if ((i).remainder(lowerTicks[0].getPosition()) == 0) {
          //Draw a foot/inches tick
          canvas.drawLine(Offset((widget.width * (i) / 10000), height),
              Offset((widget.width * (i) / 10000), height - lineHeight), paint);
        }
      }
    }

    setState(() {
      picture = recorder.endRecording();
    });
  }

  @override
  void initState() {
    _controller = AnimationController(vsync: this);
    _loadImage();
    _controller.addListener(() {
      currentLocation =
          _animation.value; //Required variable modified by animation.
      setState(() {});
    });
    super.initState();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    Size size = MediaQuery.of(context).size;
    double width = MediaQuery.of(context).size.width;
    return GestureDetector(
      onHorizontalDragUpdate: (DragUpdateDetails dragUpdate) {
        dx = dragUpdate.delta.dx;
        currentLocation =
            currentLocation - dx; //Required variable modified by pointer drag.
        _controller.stop();
        setState(() {});
      },
      onHorizontalDragEnd: (DragEndDetails dragUpdate) {
        velocity = dragUpdate.velocity.pixelsPerSecond;
        _runAnimation(velocity, size);
      },
      child: Container(
        width: width,
        height: 50,
        child: Stack(
          children: <Widget>[
            Container(
              width: width,
              height: 50,
              child: CustomPaint(
                  painter:
                      NewScrollerPainter(currentLocation, picture, _controller),
                  size: Size.fromWidth(width)),
            ),
          ],
        ),
      ),
    );
  }
}

有趣的是,在热重载后删除initState中的setState()的方法很有用,但在热重启动后却不能,尽管我不确定这如何反映问题的性质. 为了完整起见,并且如果您想测试代码,这是我的NewScrollerPainter代码:

Removing the setState() in initState works, interestingly, after a hot reload, but not after a hot restart, though I'm not sure how that reflects on the nature of the problem. For the sake of completion, and in case you would like to test the code, here's my code for my NewScrollerPainter:

class NewScrollerPainter extends CustomPainter {
  AnimationController repaint;
  double offset;
  ui.Picture img;
  double minimumExtent = 0;
  double maximumExtent = 5;
  double currentLocation;
  NewScrollerPainter(this.currentLocation, this.img, this.repaint)
      : super(repaint: repaint);
  @override
  void paint(Canvas canvas, Size size) {
    int currentSet = (currentLocation / size.width).floor();
    double currentProgress = currentLocation / size.width - currentSet;
    canvas.translate(size.width / 2, 0);
    canvas.translate(-currentProgress * size.width, 0);
    if (currentSet >= minimumExtent && currentSet < maximumExtent) {
      canvas.drawPicture(img);
    }
    if (currentProgress > 0.5 &&
        currentSet < maximumExtent - 1 &&
        currentSet > minimumExtent - 2) {
      canvas.translate(size.width, 0);
      canvas.drawPicture(img);
    } else if (currentProgress < 0.5 &&
        currentSet > 0 &&
        currentSet < maximumExtent + 1) {
      canvas.translate(-size.width, 0);
      canvas.drawPicture(img);
    }
  }

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

使用_controller.value替换NewScrollerPainter中的currentLocation变量只会冻结滚动条,而将其替换为_animation.value似乎会立即使动画值的结尾在帧之间为零.

Replacing the currentLocation variable in NewScrollerPainter with _controller.value just freezes the scroller, replacing it with _animation.value seems to instantly take the end of animation value with zero inbetween frames.

推荐答案

将NewScrollerPainter中的currentLocation变量替换为 _controller.value只是冻结了滚动条,将其替换为_animation.value似乎会立即使动画值的结尾在帧之间为零.

Replacing the currentLocation variable in NewScrollerPainter with _controller.value just freezes the scroller, replacing it with _animation.value seems to instantly take the end of animation value with zero inbetween frames.

那是因为在输入onHorizo​​ntalDragUpdate回调时,您从未真正进入侦听器.如果您看到回调

That's because you never really enter the listener when entering onHorizontalDragUpdate callback. If you see your callback

onHorizontalDragUpdate: (DragUpdateDetails dragUpdate) {
    dx = dragUpdate.delta.dx;
    currentLocation = currentLocation - dx; 
    //You're not changing its value to the controller's value, 
    //you're just depending of the dx and setState after that
    _controller.stop();
    setState(() {});
},

_controller.addListener(() { 
      //You never really triggers this when calling onHorizontalDragUpdate 
      // the controller it's never moving and calling the listener
      currentLocation =
          _animation.value; //Required variable modified by animation.
      setState(() {});
 });

init中的addListener永远不会运行(直到DragEnd因为在_runAnimation方法中运行动画并触发侦听器). currentLocation是可以无限扩展的双精度型,但是controller.value的uppderbound为1(从0到1),创建控制器时尝试将upperBound更改为infinite或大数,然后在您的控制器中使用它customPaint.

The addListener in your init never runs (until the DragEnd becasue you run the animation in the _runAnimation method and that fires the listener). The currentLocation is a double that can go infinitely, but the controller.value has its uppderbound to 1 (it runs from 0 to 1), try changing the upperBound when creating the controller to infinite or a big number, and then use it in your customPaint.

此外,如果您想在CustomPaint之前构建标尺(我想这就是您要绘制的标尺)并在手势触发动画时进行平移,我建议您使用FutureBuilder而不是在最后运行setState _loadImage.想一想如果设备速度慢或者绘图太昂贵并且在创建NewScrollerPainter之前没有结束,会发生什么情况,一秒钟之内会出现null异常(否则可能会崩溃),因为您的绘画还没有准备好.在initState中创建一个完成器,并将future方法传递给完成(_loadImage),然后在FutureBuilder中等待该完成器的将来

Also if you want to build the ruler (I think that's what you're trying to paint) before the CustomPaint and just translate it when the gesture fires the animation, I would recommend using a FutureBuilder instead of running setState at the end of _loadImage. Think what could happen if the device is slow or the draw is too expensive and didn't end before creating NewScrollerPainter, you'll have null exception for a split of a second (or it could crash) because your paint is not ready yet. Create a completer in the initState and pass the future method to the complete (_loadImage) and in the FutureBuilder wait for the future of that completer

import 'package:flutter/material.dart';
import 'package:flutter/physics.dart';
import 'dart:ui' as ui;
import 'dart:async';

class CustomScroller extends StatefulWidget {
  final double width; //Should be width of user's screen
  CustomScroller(this.width);
  @override
  _CustomScrollerState createState() => _CustomScrollerState();
}

class _CustomScrollerState extends State<CustomScroller>
    with SingleTickerProviderStateMixin {
  Completer _completer;
  double height = 50;
  AnimationController _controller;
  double dx = 0;
  Offset velocity = Offset(0, 0);
  bool loading = true; //you can use the completer to check if the paint is ready with _completer.isCompleted
  List<PaintTick> upperTicks = [
    PaintTick(100, 4), //1cm,
    PaintTick(1000, 8), //10cm
    PaintTick(5000, 12), //50cm
    PaintTick(10000, 16) //1m
  ];
  List<PaintTick> lowerTicks = [
    PaintTick(254, 5), //1 inch
    PaintTick(3048, 10), //1 foot
  ];

  /// Calculates and runs a [SpringSimulation].
  void _runAnimation(Offset pixelsPerSecond, Size size) {
    // Calculate the velocity relative to the unit interval, [0,1],
    // used by the animation controller.
    final unitsPerSecondX = pixelsPerSecond.dx / size.width;
    final unitsPerSecondY = pixelsPerSecond.dy / size.height;
    //print(unitsPerSecondY);
    final unitsPerSecond = Offset(unitsPerSecondX, unitsPerSecondY);
    final unitVelocity = unitsPerSecond.distance;

    const spring = SpringDescription(
      mass: 30,
      stiffness: 1,
      damping: 0.5,
    );

    final simulation = SpringSimulation(spring, _controller.value, 0, -unitVelocity);
    //the start position should be the place where the ruler right now (so it looks like a smooth animation), so _controller.value
    //the end position is where do you want it to end, if it's a spring I suppose you want to return to its origin, so 0

    _controller.animateWith(simulation);
  }

  Future<ui.Picture> _loadImage() async {
    ui.PictureRecorder recorder = ui.PictureRecorder();
    Canvas canvas = Canvas(recorder);

    if (lowerTicks == null) {
      lowerTicks = upperTicks;
    }
    double lineHeight = 0;

    Color lineColor = Colors.blueGrey[300];

    Paint paint = Paint()
      ..style = PaintingStyle.stroke
      ..strokeWidth = 1.2
      ..color = lineColor
      ..strokeCap = StrokeCap.round;

    for (int i = 1; i <=10000; i++){
      if (upperTicks != null) {
        for (int j = 0; j < upperTicks.length; j++) {
          if ((i).remainder(upperTicks[j].getPosition()) == 0) {
            lineHeight = (upperTicks[j].getHeight());
          }
        }
        //Position to draw
        if (i.remainder(upperTicks[0].getPosition()) == 0) {
          //Draw a meters tick
          canvas.drawLine(Offset(widget.width * (i) / 10000, 0),
              Offset(widget.width * (i) / 10000, lineHeight), paint);
        }
      }
      if (lowerTicks != null) {
        for (int j = 0; j < lowerTicks.length; j++) {
          if ((i).remainder(lowerTicks[j].getPosition()) == 0) {
            lineHeight = (lowerTicks[j].getHeight());
          }
        }

        if ((i).remainder(lowerTicks[0].getPosition()) == 0) {
          //Draw a foot/inches tick
          canvas.drawLine(Offset((widget.width * (i) / 10000), height),
              Offset((widget.width * (i) / 10000), height - lineHeight), paint);
        }
      }
    }
    return recorder.endRecording();
  }

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this, duration: Duration(seconds: 1), upperBound: double.maxFinite);
    //give an uppderBound of a big number or double.infinity
    //_controller = AnimationController.unbounded(vsync: this, duration: Duration(seconds: 1));
    //or you can use the constructor that gives you no bound like the comment above
    _completer = Completer<ui.Picture>()..complete(_loadImage()); //gives the future to complete to the completer
  }

  @override
  void dispose(){
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    Size size = MediaQuery.of(context).size;
    return GestureDetector(
      onHorizontalDragUpdate: (DragUpdateDetails dragUpdate) =>
        _controller.value -= dragUpdate.primaryDelta,
      onHorizontalDragEnd: (DragEndDetails dragUpdate) {
        velocity = dragUpdate.velocity.pixelsPerSecond;
        _runAnimation(velocity, size);
      },
      child: Stack(
        children: <Widget>[
          Container(
            width: widget.width,
            height: 50,
            child: FutureBuilder<ui.Picture>(
                future: _completer.future, //wait for the future to complete
                builder: (context, snapshot){
                  if(snapshot.hasData) //when the future completes you can use the Picture
                    return CustomPaint(
                        painter: NewScrollerPainter(snapshot.data, _controller),
                        size: Size.fromWidth(widget.width)
                    );
                  return const SizedBox(); //if it's not complete just return an empty SizedBox until it finished
                }
            ),
          ),
        ],
      ),
    );
  }
}

看看我是如何从代码中删除currentLocation的,不再需要它了,因为您将使用controller.value来更改位置

See how I deleted currentLocation from the code, there is no need of it anymore because you will use the controller.value to change the position

class NewScrollerPainter extends CustomPainter {
  Animation<double> repaint;
  double offset;
  ui.Picture img;
  double minimumExtent = 0;
  double maximumExtent = 5;
  NewScrollerPainter(this.img, this.repaint)
      : super(repaint: repaint);

  @override
  void paint(Canvas canvas, Size size) {
    int currentSet = (repaint.value / size.width).floor(); //now it will give you the real value
    double currentProgress = repaint.value / size.width - currentSet; //now it will give you the real value
    canvas.translate(size.width / 2, 0);
    canvas.translate(-currentProgress * size.width, 0);
    if (currentSet >= minimumExtent && currentSet < maximumExtent) {
      canvas.drawPicture(img);
    }
    if (currentProgress > 0.5 &&
        currentSet < maximumExtent - 1 &&
        currentSet > minimumExtent - 2) {
      canvas.translate(size.width, 0);
      canvas.drawPicture(img);
    } else if (currentProgress < 0.5 &&
        currentSet > 0 &&
        currentSet < maximumExtent + 1) {
      canvas.translate(-size.width, 0);
      canvas.drawPicture(img);
    }
  }

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

这篇关于使用重绘为CustomPainter动画,同时还可以在Flutter中检索Controller.value的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

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