MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Flutter减少重绘的动画优化:实现流畅的动画效果

2023-08-218.0k 阅读

Flutter 动画基础概述

在 Flutter 开发中,动画是提升用户体验的重要手段。Flutter 提供了丰富的动画库,使得开发者能够轻松创建各种动画效果。动画的本质是在一段时间内改变一个或多个属性的值,比如位置、大小、透明度等。Flutter 中的动画主要基于 Animation 类及其相关子类实现。

Animation 类本身是一个抽象类,它表示一个随时间变化的数值。例如,Animation<double> 可以表示一个从 0.0 到 1.0 变化的双精度浮点数,这个数值可以用来控制 UI 元素的属性。通常,我们会使用 AnimationController 来驱动 AnimationAnimationController 继承自 Animation<double>,它除了具备 Animation 的功能外,还可以控制动画的播放、暂停、反向播放等操作。

以下是一个简单的示例,展示如何创建一个基本的动画控制器和动画:

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(seconds: 2),
      vsync: this,
    );
    _animation = Tween<double>(begin: 0.0, end: 1.0).animate(_controller);
    _controller.forward();
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Basic Animation'),
      ),
      body: Center(
        child: AnimatedBuilder(
          animation: _animation,
          builder: (context, child) {
            return Opacity(
              opacity: _animation.value,
              child: child,
            );
          },
          child: Container(
            width: 200,
            height: 200,
            color: Colors.blue,
          ),
        ),
      ),
    );
  }
}

在上述代码中,我们创建了一个 AnimationController,其持续时间为 2 秒。然后通过 Tween 创建了一个从 0.0 到 1.0 的 Animation<double>AnimatedBuilder 会根据 _animation 的值变化来重建其内部的 Opacity 组件,从而实现透明度从 0 到 1 的动画效果。

重绘原理及在 Flutter 中的表现

在 Flutter 中,重绘是指当 Widget 的状态或属性发生变化时,Flutter 框架会重新构建 Widget 树,并将新的 UI 绘制到屏幕上。重绘过程主要分为三个阶段:布局(Layout)、绘制(Paint)和合成(Composite)。

布局阶段,Flutter 会根据父 Widget 的约束和子 Widget 的大小需求,确定每个 Widget 在屏幕上的位置和大小。绘制阶段,每个 Widget 会将自己绘制到一个画布(Canvas)上。最后,合成阶段会将所有绘制好的画布合并成最终的屏幕图像。

当动画运行时,如果频繁触发重绘,会导致性能问题,因为每次重绘都需要重新执行布局、绘制和合成操作。在 Flutter 中,一些常见的导致重绘的原因包括:

  1. State 变化:当 StatefulWidgetState 发生变化时,会触发重绘。例如,在上面的动画示例中,如果我们在 _MyHomePageState 中改变了一个影响 UI 显示的变量,就会导致重绘。
  2. 父 Widget 重建:如果父 Widget 重建,其所有子 Widget 通常也会重建并触发重绘。这可能是因为父 Widget 的 State 变化,或者父 Widget 所在的 InheritedWidget 发生了变化。
  3. 动画属性更新:每次动画值更新时,如果依赖该动画值的 Widget 没有进行优化,就会触发重绘。例如,在上面的 AnimatedBuilder 示例中,如果 _animation 的值变化,AnimatedBuilder 内部的 Opacity 组件就会重绘。

Flutter 减少重绘的动画优化策略

  1. 使用 AnimatedWidget 及其子类
    • AnimatedWidget 是一个抽象类,它简化了基于 Animation 的 Widget 构建。它只会在 Animation 的值发生变化时才重建,而不是像普通 StatefulWidget 那样在任何状态变化时都重建。AnimatedOpacityAnimatedContainer 等都是 AnimatedWidget 的具体子类,使用它们可以减少不必要的重绘。
    • 例如,我们可以将前面的示例改写为使用 AnimatedOpacity
import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(seconds: 2),
      vsync: this,
    );
    _animation = Tween<double>(begin: 0.0, end: 1.0).animate(_controller);
    _controller.forward();
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('AnimatedOpacity Example'),
      ),
      body: Center(
        child: AnimatedOpacity(
          opacity: _animation.value,
          duration: const Duration(seconds: 2),
          child: Container(
            width: 200,
            height: 200,
            color: Colors.blue,
          ),
        ),
      ),
    );
  }
}
  • 在这个示例中,AnimatedOpacity 只会在 _animation 的值变化时才会更新 opacity 属性并触发重绘,相比于之前使用 AnimatedBuilder 包裹 Opacity,减少了不必要的重绘。因为 AnimatedOpacity 内部对重绘进行了优化,只有与动画相关的属性变化时才会触发重绘。
  1. 局部刷新策略
    • 在 Flutter 中,尽量避免整个 Widget 树的重建,而是只更新需要改变的部分。例如,使用 CustomSingleChildLayout 可以在不重建整个父 Widget 的情况下,局部更新子 Widget 的布局。
    • 假设我们有一个包含多个子 Widget 的父 Widget,其中只有一个子 Widget 参与动画。我们可以将参与动画的子 Widget 放在 CustomSingleChildLayout 中,这样当动画运行时,只有这个子 Widget 所在的局部区域会被更新,而不是整个父 Widget 及其所有子 Widget 都被重建。
    • 以下是一个简单示例,展示如何使用 CustomSingleChildLayout 实现局部刷新:
import 'package:flutter/material.dart';

class AnimatedChildLayout extends SingleChildLayoutDelegate {
  final double animationValue;

  AnimatedChildLayout(this.animationValue);

  @override
  BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
    return BoxConstraints.tightFor(width: 100, height: 100);
  }

  @override
  Offset getPositionForChild(Size size, Size childSize) {
    return Offset(animationValue * (size.width - childSize.width), 0);
  }

  @override
  bool shouldRelayout(covariant SingleChildLayoutDelegate oldDelegate) {
    return oldDelegate is AnimatedChildLayout &&
        (oldDelegate.animationValue != animationValue);
  }
}

class MyAnimatedWidget extends StatefulWidget {
  @override
  _MyAnimatedWidgetState createState() => _MyAnimatedWidgetState();
}

class _MyAnimatedWidgetState extends State<MyAnimatedWidget> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(seconds: 3),
      vsync: this,
    );
    _animation = Tween<double>(begin: 0.0, end: 1.0).animate(_controller);
    _controller.forward();
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Local Refresh Example'),
      ),
      body: Stack(
        children: [
          Container(
            color: Colors.grey[200],
            width: double.infinity,
            height: double.infinity,
          ),
          CustomSingleChildLayout(
            delegate: AnimatedChildLayout(_animation.value),
            child: Container(
              color: Colors.blue,
            ),
          ),
        ],
      ),
    );
  }
}
  • 在这个示例中,CustomSingleChildLayoutAnimatedChildLayout 委托会根据 _animation.value 来确定子 Container 的位置。当动画运行时,只有 CustomSingleChildLayout 内部的子 Container 会根据动画值的变化进行位置更新,而不会影响到整个 Stack 及其它子 Widget,从而实现了局部刷新,减少了重绘。
  1. 缓存绘制结果
    • Flutter 提供了 RepaintBoundary 组件,它可以作为一个绘制边界,将其内部的 Widget 绘制结果缓存起来。当 RepaintBoundary 内部的 Widget 状态没有变化时,不会触发重绘,而是直接使用缓存的绘制结果。
    • 例如,在一个包含动画的复杂界面中,如果有一些 Widget 的内容在动画过程中不会改变,我们可以将这些 Widget 包裹在 RepaintBoundary 中。
import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(seconds: 2),
      vsync: this,
    );
    _animation = Tween<double>(begin: 0.0, end: 1.0).animate(_controller);
    _controller.forward();
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('RepaintBoundary Example'),
      ),
      body: Column(
        children: [
          RepaintBoundary(
            child: Container(
              width: double.infinity,
              height: 100,
              color: Colors.red,
              child: Center(
                child: Text('This part is cached'),
              ),
            ),
          ),
          AnimatedBuilder(
            animation: _animation,
            builder: (context, child) {
              return Opacity(
                opacity: _animation.value,
                child: child,
              );
            },
            child: Container(
              width: 200,
              height: 200,
              color: Colors.blue,
            ),
          ),
        ],
      ),
    );
  }
}
  • 在这个示例中,红色背景且包含文本的 Container 被包裹在 RepaintBoundary 中。在动画运行过程中,即使 AnimatedBuilder 中的 Opacity 动画会触发重绘,但 RepaintBoundary 内部的内容由于没有状态变化,不会被重绘,而是使用缓存的绘制结果,从而减少了整体的重绘次数。
  1. 优化动画曲线和帧率
    • 动画曲线:选择合适的动画曲线可以提升动画的流畅度并减少不必要的重绘。Flutter 提供了多种预定义的动画曲线,如 Curves.linear(线性变化)、Curves.easeInOut(缓入缓出)等。不合适的动画曲线可能导致动画在某些阶段变化过于剧烈,从而触发更多的重绘。例如,如果一个动画在开始和结束时变化过快,可能会使得 Widget 的布局和绘制在短时间内频繁调整,增加重绘次数。通过选择合适的缓动曲线,如 Curves.fastOutSlowIn,可以让动画在开始时快速变化,结束时缓慢变化,使动画过程更加自然,同时减少因过度变化导致的重绘。
    • 帧率:Flutter 默认的动画帧率是 60fps。在一些性能较差的设备上,过高的帧率可能无法维持,导致动画卡顿。可以通过调整动画的帧率来平衡性能和流畅度。例如,在一些低端设备上,可以将帧率降低到 30fps。这可以通过 AnimationControllerlowerBoundupperBound 属性来间接控制。例如,将 AnimationControllerduration 设置得更长,在相同的动画时间内,帧率就会相对降低。同时,一些动画库也提供了直接设置帧率的方法。在降低帧率时,要注意动画的流畅度,避免出现明显的卡顿感。

综合案例:复杂动画场景下的优化

假设我们要创建一个类似游戏界面的动画场景,其中有多个角色在屏幕上移动,并且场景中有一些背景特效和 UI 元素。

  1. 初始未优化版本
import 'package:flutter/material.dart';

class Character {
  double x;
  double y;
  Character(this.x, this.y);
}

class GameScene extends StatefulWidget {
  @override
  _GameSceneState createState() => _GameSceneState();
}

class _GameSceneState extends State<GameScene> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  List<Character> characters = [
    Character(100, 100),
    Character(200, 200),
  ];

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(seconds: 5),
      vsync: this,
    );
    _controller.addListener(() {
      setState(() {
        for (var character in characters) {
          character.x += 1;
          character.y += 1;
        }
      });
    });
    _controller.forward();
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Game Scene'),
      ),
      body: Stack(
        children: [
          Container(
            color: Colors.black,
          ),
          for (var character in characters)
            Positioned(
              left: character.x,
              top: character.y,
              child: Container(
                width: 50,
                height: 50,
                color: Colors.red,
              ),
            ),
        ],
      ),
    );
  }
}

在这个初始版本中,每次动画值更新时,通过 setState 来更新所有角色的位置,这会导致整个 Stack 及其子 Widget 都被重建,重绘次数较多,性能较差。 2. 优化版本

import 'package:flutter/material.dart';

class Character {
  double x;
  double y;
  Character(this.x, this.y);
}

class CharacterLayout extends SingleChildLayoutDelegate {
  final double x;
  final double y;

  CharacterLayout(this.x, this.y);

  @override
  BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
    return BoxConstraints.tightFor(width: 50, height: 50);
  }

  @override
  Offset getPositionForChild(Size size, Size childSize) {
    return Offset(x, y);
  }

  @override
  bool shouldRelayout(covariant SingleChildLayoutDelegate oldDelegate) {
    return oldDelegate is CharacterLayout &&
        (oldDelegate.x != x || oldDelegate.y != y);
  }
}

class GameScene extends StatefulWidget {
  @override
  _GameSceneState createState() => _GameSceneState();
}

class _GameSceneState extends State<GameScene> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  List<Character> characters = [
    Character(100, 100),
    Character(200, 200),
  ];

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(seconds: 5),
      vsync: this,
    );
    _controller.addListener(() {
      setState(() {
        for (var character in characters) {
          character.x += 1;
          character.y += 1;
        }
      });
    });
    _controller.forward();
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Optimized Game Scene'),
      ),
      body: Stack(
        children: [
          RepaintBoundary(
            child: Container(
              color: Colors.black,
            ),
          ),
          for (var character in characters)
            CustomSingleChildLayout(
              delegate: CharacterLayout(character.x, character.y),
              child: Container(
                color: Colors.red,
              ),
            ),
        ],
      ),
    );
  }
}

在优化版本中,我们使用了 CustomSingleChildLayout 来实现每个角色的局部布局更新,并且将背景 Container 包裹在 RepaintBoundary 中。这样,当角色位置更新时,只有每个角色对应的 CustomSingleChildLayout 内部会重绘,背景部分由于被缓存不会重绘,大大减少了重绘次数,提升了动画的流畅度。同时,如果角色的动画有不同的曲线需求,可以分别为每个角色的动画设置合适的动画曲线,进一步优化动画效果。

通过以上这些优化策略,在 Flutter 前端开发中,我们可以有效地减少动画过程中的重绘次数,实现更加流畅的动画效果,提升用户体验。在实际项目中,需要根据具体的动画场景和需求,灵活选择和组合这些优化方法,以达到最佳的性能表现。