Original link: medium.com/mobile-deve…

This article will show how to use Flutter to animate the following effects. The Demo code for this article can be found in the Pub’s Explode_view project.

We’ll start by creating an ExplodeView object, which basically holds the imagePath and the location of the image in the Widget.

class ExplodeView extends StatelessWidget {

  final String imagePath;

  final double imagePosFromLeft;

  final double imagePosFromTop;

  const ExplodeView({
    @required this.imagePath,
    @required this.imagePosFromLeft,
    @required this.imagePosFromTop
  });
  
  @override
  Widget build(BuildContext context) {
    // This variable contains the size of the screen
    final screenSize = MediaQuery.of(context).size;

    returnnew Container( child: new ExplodeViewBody( screenSize: screenSize, imagePath: imagePath, imagePosFromLeft: imagePosFromLeft, imagePosFromTop: imagePosFromTop), ); }}Copy the code

Then we start implementing It ExplodeViewBody, looking at its State implementation, which inherits State and uses TickerProviderStateMixin to implement the animation.

class _ExplodeViewState extends State<ExplodeViewBody> with TickerProviderStateMixin{
    
    GlobalKey currentKey;
    GlobalKey imageKey = GlobalKey();
    GlobalKey paintKey = GlobalKey();
    
    bool useSnapshot = true;
    bool isImage = true; math.Random random; img.Image photo; AnimationController imageAnimationController; Double imageSize = 50.0; Double distFromLeft = 10.0, distFromTop = 10.0; final StreamController<Color> _stateController = StreamController<Color>.broadcast(); @override voidinitState() {
        super.initState();
    
        currentKey = useSnapshot ? paintKey : imageKey;
        random = new math.Random();
    
        imageAnimationController = AnimationController(
          vsync: this,
          duration: Duration(milliseconds: 3000),
        );
    
      }
    
      @override
      Widget build(BuildContext context) {
        return Container(
          child: isImage
              ? StreamBuilder(
            initialData: Colors.green[500],
            stream: _stateController.stream,
            builder: (buildContext, snapshot) {
              return Stack(
                children: <Widget>[
                  RepaintBoundary(
                    key: paintKey,
                    child: GestureDetector(
                      onLongPress: () async {
                       //do explode
                      }
                      child: Container(
                        alignment: FractionalOffset((widget.imagePosFromLeft / widget.screenSize.width), (widget.imagePosFromTop / widget.screenSize.height)),
                        child: Transform(
                          transform: Matrix4.translation(_shakeImage()),
                          child: Image.asset(
                            widget.imagePath,
                            key: imageKey,
                            width: imageSize,
                            height: imageSize,
                          ),
                        ),
                      ),
                    ),
                  )
                ],
              );
            },
          ):
              Container(
                child: Stack(
                  children: <Widget>[
                    for(Particle particle in particles) particle.startParticleAnimation()
                  ],
                ),
              )
        );
      }
    
      @override
      void dispose(){ imageAnimationController.dispose(); super.dispose(); }}Copy the code

Part of the code is omitted here, which will be described later.

First, we initialize the StreamController
object in _ExplodeViewState, which controls the StreamBuilder to trigger UI redraws via the Stream Stream.

Then, in the initState method, we initialize the imageAnimationController as an animation controller that controls the jiggling of the image before it explodes.

Then, in the build method, determine whether the Image or particle animation needs to be displayed according to the conditions. If the Image needs to be displayed, use image. asset to display the Image effect. The outer GestureDetector is used to trigger the explosion animation effect for a long time; Stream in StreamBuilder is used to save the color of the picture and control the execution of the redraw.

Next we need to implement Particle objects, which are used to animate each Particle.

In the constructor for Particle, you specify the ID (index in the Demo), color, and Particle position as parameters, and then initialize an AnimationController to control Particle movement, as shown in the code below. You can set the Tween to move the animation along your positive and negative X and y axes, as well as the opacity of the particles during the animation.

Particle({@required this.id, @required this.screenSize, this.colors, this.offsetX, this.offsetY, this.newOffsetX, this.newOffsetY}) {

  position = Offset(this.offsetX, this.offsetY);

  math.Random random = new math.Random();
  this.lastXOffset = random.nextDouble() * 100;
  this.lastYOffset = random.nextDouble() * 100;

  animationController = new AnimationController(
      vsync: this,
      duration: Duration(milliseconds: 1500)
  );

  translateXAnimation = Tween(begin: position.dx, end: lastXOffset).animate(animationController);
  translateYAnimation = Tween(begin: position.dy, end: lastYOffset).animate(animationController);
  negatetranslateXAnimation = Tween(begin: -1 * position.dx, end: -1 * lastXOffset).animate(animationController);
  negatetranslateYAnimation = Tween(begin: -1 * position.dy, end: -1 * lastYOffset).animate(animationController);
  fadingAnimation = Tween<double>(
    begin: 1.0,
    end: 0.0,
  ).animate(animationController);

  particleSize = Tween(begin: 5.0, end: random.nextDouble() * 20).animate(animationController);

}
Copy the code

We then implement the startParticleAnimation() method, which performs the particle animation by adding the above animationController to the AnimatedBuilder control and executing it. Then, Transform and FadeTransition are combined with the Builder method of AnimatedBuilder to achieve the animation movement and transparency change effect.

 startParticleAnimation() {
    animationController.forward();

    return Container(
      alignment: FractionalOffset(
          (newOffsetX / screenSize.width), (newOffsetY / screenSize.height)),
      child: AnimatedBuilder(
        animation: animationController,
        builder: (BuildContext context, Widget widget) {
          if (id % 4 == 0) {
            return Transform.translate(
                offset: Offset(
                    translateXAnimation.value, translateYAnimation.value),
                child: FadeTransition(
                  opacity: fadingAnimation,
                  child: Container(
                    width: particleSize.value > 5 ? particleSize.value : 5,
                    height: particleSize.value > 5 ? particleSize.value : 5,
                    decoration:
                        BoxDecoration(color: colors, shape: BoxShape.circle),
                  ),
                ));
          } else if (id % 4 == 1) {
            return Transform.translate(
                offset: Offset(
                    negatetranslateXAnimation.value, translateYAnimation.value),
                child: FadeTransition(
                  opacity: fadingAnimation,
                  child: Container(
                    width: particleSize.value > 5 ? particleSize.value : 5,
                    height: particleSize.value > 5 ? particleSize.value : 5,
                    decoration:
                        BoxDecoration(color: colors, shape: BoxShape.circle),
                  ),
                ));
          } else if (id % 4 == 2) {
            return Transform.translate(
                offset: Offset(
                    translateXAnimation.value, negatetranslateYAnimation.value),
                child: FadeTransition(
                  opacity: fadingAnimation,
                  child: Container(
                    width: particleSize.value > 5 ? particleSize.value : 5,
                    height: particleSize.value > 5 ? particleSize.value : 5,
                    decoration:
                        BoxDecoration(color: colors, shape: BoxShape.circle),
                  ),
                ));
          } else {
            returnTransform.translate( offset: Offset(negatetranslateXAnimation.value, negatetranslateYAnimation.value), child: FadeTransition( opacity: fadingAnimation, child: Container( width: particleSize.value > 5 ? particleSize.value : 5, height: particleSize.value > 5 ? particleSize.value : 5, decoration: BoxDecoration(color: colors, shape: BoxShape.circle), ), )); }},),); })Copy the code

As shown in the code above, the example movement in four different directions is implemented by using different orientation values and offsets, then configuring the animation according to the Tween object defined above, and finally creating the particles using a circular shape BoxDecoration and variable height and width.

This completes the implementation of the Particle class, and the implementation of getting the color from the image is next.

Future<Color> getPixel(Offset globalPosition, Offset position, double size) async {
  if (photo == null) {
    await (useSnapshot ? loadSnapshotBytes() : loadImageBundleBytes());
  }

  Color newColor = calculatePixel(globalPosition, position, size);
  return newColor;
}

Color calculatePixel(Offset globalPosition, Offset position, double size) {

  double px = position.dx;
  double py = position.dy;


  if(! useSnapshot) { double widgetScale = size / photo.width; px = (px / widgetScale); py = (py / widgetScale); } int pixel32 = photo.getPixelSafe(px.toInt()+1, py.toInt()); int hex = abgrToArgb(pixel32); _stateController.add(Color(hex)); ColorreturnColor = Color(hex);

  return returnColor;
}
Copy the code

The code shown above, which implements the pixel color from the image at a specified position, uses different methods in Demo to load and set the image bytes (loadSnapshotBytes() or loadImageBundleBytes()) to get the color data.

// Loads the bytes of the image and sets it in the img.Image object
  Future<void> loadImageBundleBytes() async {
    ByteData imageBytes = await rootBundle.load(widget.imagePath);
    setImageBytes(imageBytes);
  }

  // Loads the bytes of the snapshot if the img.Image object is null
  Future<void> loadSnapshotBytes() async {
    RenderRepaintBoundary boxPaint = paintKey.currentContext.findRenderObject();
    ui.Image capture = await boxPaint.toImage();
    ByteData imageBytes =
        await capture.toByteData(format: ui.ImageByteFormat.png);
    setImageBytes(imageBytes);
    capture.dispose();
  }
  
  void setImageBytes(ByteData imageBytes) {
    List<int> values = imageBytes.buffer.asUint8List();
    photo = img.decodeImage(values);
  }

Copy the code

Now when we hold down the image, we can go to the final animation of the scattered particles and start generating the particles by executing the following method:

RenderBox box = imageKey.currentContext.findRenderObject();
Offset imagePosition = box.localToGlobal(Offset.zero);
double imagePositionOffsetX = imagePosition.dx;
double imagePositionOffsetY = imagePosition.dy;

double imageCenterPositionX = imagePositionOffsetX + (imageSize / 2);
double imageCenterPositionY = imagePositionOffsetY + (imageSize / 2);
for(int i = 0; i < noOfParticles; i++){
  if(I < 21){getPixel(imagePosition, Offset(imagePositionOffsetX + (I * 0.7), imagePositionOffsety-60), box.size.width).then((value) { colors.add(value); }); }else if(I >= 21 && I < 42){getPixel(imagePosition, Offset(imagePositionOffsetX + (I * 0.7), imagePositionOffsety-52), box.size.width).then((value) { colors.add(value); }); }else{getPixel(imagePosition, Offset(imagePositionOffsetX + (I * 0.7), imagePositionOffseTY-68), box.size.width).then((value) { colors.add(value); }); } } Future.delayed(Duration(milliseconds: 3500), () {for(int i = 0; i < noOfParticles; i++){
    if(I < 21){particles. Add (Particle(id: I, screenSize: widget. ScreenSize, colors: colors[I]. (imageCenterPositionX - imagePositionOffsetX + (I * 0.7)) * 0.1, offsetY: (imageCenterPositionY - (imagePositionOffsetY - 60)) * 0.1, newOffsetX: imagePositionOffsetX + (I * 0.7), newOffsetY: imagePositionOffsetY - 60)); }else if(i >= 21 && i < 42){ particles.add(Particle(id: i, screenSize: widget.screenSize, colors: Opacity: (imageCenterPositionX - imagePositionOffsetX + (I * 0.5)) * 0.1; (imageCenterPositionY - (imagePositionOffsetY - 52)) * 0.1, newOffsetX: imagePositionOffsetX + (I * 0.7), newOffsetY: imagePositionOffsetY - 52)); }else{particles. The add (Particle (id: I, screenSize: widget. The screenSize, colors: colors [I] withOpacity (1.0), offsetX: (imageCenterPositionX - imagePositionOffsetX + (I * 0.9)) * 0.1, offsetY: (imageCenterPositionY - (imagePositionOffsetY - 68)) * 0.1, newOffsetX: imagePositionOffsetX + (I * 0.7), newOffsetY: imagePositionOffsetY - 68)); }}setState(() {
    isImage = false;
  });
});
Copy the code

As shown in the code above, we use the RenderBox class to get the position of the image and then get the color from the getPixel() method defined above.

The obtained colors are extracted from three horizontal lines on the image, using random offsets on the same line so that more colors can be obtained from the image, and then using Particle to create particles at different positions with the appropriate parameter values.

Of course, there is a 3.5 second delay in execution, and during this delay the image shakes. Jitter can be easily implemented by using the Matrix4.translation() method, which uses a different offset than the _shakeImage method shown below to quickly transform the image.

Vector3 _shakeImage() {
  returnVector3 (math. Sin ((imageAnimationController. Value) * math.h PI * 20.0) * 8, 0.0, 0.0); }Copy the code

Finally, after shaking the image and creating particles, the image disappears and the previous startParticleAnimation method is called, which completes the image explosion in the Flutter.

And finally, I can introduce ExplodeView as follows.

ExplodeView(
   imagePath: path, 
   imagePosFromLeft: xxxx, 
   imagePosFromTop: xxxx
),
Copy the code

Demo address: github.com/mdg-soc-19/…

Ps: Unlike the 2d pixels on the vertical and horizontal coordinates of the Bitmap on Android, there is no way to achieve the effect of the whole image exploding in place.

Resources to recommend

  • Making: github.com/CarGuo
  • Open Source Flutter complete project:Github.com/CarGuo/GSYG…
  • Open Source Flutter Multi-case learning project:Github.com/CarGuo/GSYF…
  • Open Source Fluttre Combat Ebook Project:Github.com/CarGuo/GSYF…
  • Open Source React Native project: github.com/CarGuo/GSYG…