Flutter實現簡單爆炸效果

eee發表於2021-07-05

前言

最近一個月支援某業務比較忙,並沒有比較長的時間週期讓我攻克Flutter一些比較難知識。比如我看到某開源專案自己使用Stream等技術實現了一個稍微複雜的Event Bus,我試著用碎片時間去學,結果發現需要補充的概念太多,碎片化地學習很多東西連貫不起來,於是我就暫時先放棄了,等個稍微長點的假期再來攻克吧。

最終,我覺得用這些碎片化的時間來學習Flutter的動畫,並試著做一些簡單的特效,本文就參考網上的一些教程,寫一個簡單的爆炸效果。

最終效果圖: middle_img_v2_ba180cff90f14ba6aff880a32fb84cdg.gif

原文教程連結:傳送門,本文進行了一定改造
完整程式碼:gitee.com/wenjie2018/…
Flutter SDK: 2.2.2
dependencies: simple_animations: ^3.0.1

simple_animations封裝了一些動畫邏輯,可以讓我們更方便的使用動畫的API


從一個點開始

先說說整體思路:爆炸就是中間有很多個點,它們在同一時刻往不同方向走了不同的距離,並且過程中不斷變小、透明,直到消失? image.png

在實現一群點之前,我們先來實現一個點的漸變? image.png

程式碼大致思路:

  • 構建區域迴圈動畫
  • 點選按鈕:如果掃描到List中有“點”Widget,則將這個點從左到右推出,並且逐漸變小、透明
  • 每一幀都檢查“點”的Widget是否超過動畫時間,如超過則從List中刪除

關鍵API:

  • 使用LoopAnimation構建迴圈動畫
  • 使用Color.withAlpha控制透明度
  • 使用Transform.scale控制控制元件大小
  • 使用Stack讓“點”可以重疊,再使用Positioned控制座標

完整程式碼?:


import 'dart:ui';

import 'package:flutter/material.dart';
import 'package:simple_animations/simple_animations.dart';
import 'package:supercharged_dart/supercharged_dart.dart';
import 'package:supercharged/supercharged.dart';

void main() {
  runApp(new MaterialApp(home: Page()));
}

class Page extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => PageState();
}

class PageState extends State<Page> {

  final GlobalKey<MoleState> moleKey = new GlobalKey();

  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Mole>[
            Mole(key: moleKey)
          ],
        )
        // child: Mole(key: moleKey),
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.sentiment_very_satisfied),
        onPressed: () {
          moleKey.currentState!.hitMole();
        },
      ),
    );
  }
}

class Mole extends StatefulWidget {
  @override
  MoleState createState() => MoleState();

  Mole({
    Key? key,
  }) : super(key: key);
}

class MoleState extends State<Mole> {
  /// "點"的List
  final List<MoleParticle> particles = [];

  @override
  Widget build(BuildContext context) {
    return Container(
      width: 100,
      height: 100,
      child: _buildMole(),
    );
  }

  Widget _buildMole() {
    return LoopAnimation<int>(
      tween: ConstantTween(1),
      builder: (context, child, value) {
        // builder每一幀都會被呼叫,所以要再次清理點
        _manageParticleLifecycle();
        return Stack(
          clipBehavior: Clip.none,
          children: [
            ...particles.map((it) => it.buildWidget())
          ],
        );
      },
    );
  }

  /// 向List中插入"點"
  hitMole() {
    Iterable.generate(1).forEach(
            (i) => particles.add(MoleParticle())
    );
  }

  /// 管理顆粒的生命,清除超時的顆粒
  _manageParticleLifecycle() {
    particles.removeWhere((particle) {
      return particle.progress() == 1;
    });
  }

  @override
  void setState(fn) {
    if (mounted) {
      super.setState(fn);
    }
  }
}

enum _MoleProps { x, y, scale }

class MoleParticle {
  late Animatable<MultiTweenValues<_MoleProps>> tween;
  late Duration startTime;
  final duration = 3.seconds;

  MoleParticle() {
    final start_x = 0.0;
    final start_y = 0.0;
    final end_x = window.physicalSize.width / window.devicePixelRatio - 50;
    final end_y = 0.0;

    tween = MultiTween<_MoleProps>()
      ..add(_MoleProps.x, start_x.tweenTo(end_x))
      ..add(_MoleProps.y, start_y.tweenTo(end_y))
      ..add(_MoleProps.scale, 1.0.tweenTo(0.0));

    startTime = DateTime.now().duration();
  }

  Widget buildWidget() {
    final MultiTweenValues<_MoleProps> values = tween.transform(progress());
    var alpha = 255 - (255 * progress()).toInt();
    return Positioned(
      left: values.get(_MoleProps.x),
      top: values.get(_MoleProps.y),
      child: Transform.scale(
        scale: values.get(_MoleProps.scale),
        child: Container(
          width: 100,
          height: 100,
          decoration: BoxDecoration(
              color: Colors.black.withAlpha(alpha),
              borderRadius: BorderRadius.circular(50)
          ),
        ),
      ),
    );
  }

  /// 計算"點"的生命週期
  double progress() {
    return ((DateTime.now().duration() - startTime) / duration)
        .clamp(0.0, 1.0)
        .toDouble();
  }
}

複製程式碼

效果預覽?: middle_img_v2_18d8511115d64ca4806ecef1c68b50fg.gif


實現爆炸

實現了一個“點”的漸變後,之後要實現爆炸就好辦了,大致變更如下:

  • 動畫週期3秒 -> 0.5秒
  • 一次性塞一個“點” -> 一次性塞50個“點”
  • 控制元件從靠左居中 -> 螢幕居中
  • 每個點的起始座標固定但結束座標改為隨機

最終程式碼如下:


import 'dart:math';
import 'dart:ui';

import 'package:flutter/material.dart';
import 'package:simple_animations/simple_animations.dart';
import 'package:supercharged_dart/supercharged_dart.dart';
import 'package:supercharged/supercharged.dart';

void main() {
  runApp(new MaterialApp(home: Page()));
}

class Page extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => PageState();
}

class PageState extends State<Page> {

  final GlobalKey<MoleState> moleKey = new GlobalKey();

  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Center(
          child: Mole(key: moleKey),
        )
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.sentiment_very_satisfied),
        onPressed: () {
          moleKey.currentState!.hitMole();
        },
      ),
    );
  }
}

class Mole extends StatefulWidget {
  @override
  MoleState createState() => MoleState();

  Mole({
    Key? key,
  }) : super(key: key);
}

class MoleState extends State<Mole> {
  /// "點"的List
  final List<MoleParticle> particles = [];

  @override
  Widget build(BuildContext context) {
    return Container(
      width: 100,
      height: 100,
      child: _buildMole(),
    );
  }

  Widget _buildMole() {
    return LoopAnimation<int>(
      duration: 2.seconds,
      tween: ConstantTween(1),
      builder: (context, child, value) {
        // builder每一幀都會被呼叫,所以要再次清理點
        _manageParticleLifecycle();
        return Stack(
          clipBehavior: Clip.none,
          children: [
            ...particles.map((it) => it.buildWidget())
          ],
        );
      },
    );
  }

  /// 向List中插入"點"
  hitMole() {
    Iterable.generate(50).forEach(
            (i) => particles.add(MoleParticle())
    );
  }

  /// 管理顆粒的生命,清除超時的顆粒
  _manageParticleLifecycle() {
    particles.removeWhere((particle) {
      return particle.progress() == 1;
    });
  }

  @override
  void setState(fn) {
    if (mounted) {
      super.setState(fn);
    }
  }
}

enum _MoleProps { x, y, scale }

class MoleParticle {
  late Animatable<MultiTweenValues<_MoleProps>> tween;
  late Duration startTime;
  final duration = 0.5.seconds;

  MoleParticle() {
    var random = Random();
    double max_x = 300;
    double max_y = 300;
    final start_x = 0.0;
    final start_y = 0.0;
    final end_x = max_x * random.nextDouble() * (random.nextBool() ? 1 : -1);
    final end_y = max_y * random.nextDouble() * (random.nextBool() ? 1 : -1);

    tween = MultiTween<_MoleProps>()
      ..add(_MoleProps.x, start_x.tweenTo(end_x))
      ..add(_MoleProps.y, start_y.tweenTo(end_y))
      ..add(_MoleProps.scale, 1.0.tweenTo(0.0));

    startTime = DateTime.now().duration();
  }

  Widget buildWidget() {
    final MultiTweenValues<_MoleProps> values = tween.transform(progress());
    var alpha = 255 - (255 * progress()).toInt();
    return Positioned(
      left: values.get(_MoleProps.x),
      top: values.get(_MoleProps.y),
      child: Transform.scale(
        scale: values.get(_MoleProps.scale),
        child: Container(
          width: 100,
          height: 100,
          decoration: BoxDecoration(
              color: Colors.black.withAlpha(alpha),
              borderRadius: BorderRadius.circular(50)
          ),
        ),
      ),
    );
  }

  /// 計算"點"的生命週期
  double progress() {
    return ((DateTime.now().duration() - startTime) / duration)
        .clamp(0.0, 1.0)
        .toDouble();
  }
}

複製程式碼

效果預覽?: middle_img_v2_7f8193e6e63c48f9862e3e28a270d0bg.gif

幀率?: QQ20210703151006.gif

瘋狂點選下的幀率會有所下降?: middle_img_v2_ef772b433998496bab1dd215d7eab69g.gif QQ20210703-151457

  • 這個問題暫時沒有很好的優化思路,等以後有機會再想辦法優化吧

相關文章