CustomRouteTransition:自定义 PageRouteBuilder 与 Canvas 变换

CustomRouteTransition:自定义 PageRouteBuilder 与 Canvas 变换

大家好,今天我们来深入探讨 Flutter 中自定义路由转场动画的核心技术:PageRouteBuilderCanvas 变换。我们将不再局限于 Flutter 提供的预设转场效果,而是利用这两个强大的工具,创造出独一无二、高度定制化的页面切换动画。

1. 路由转场动画的必要性

在现代移动应用开发中,流畅且引人入胜的页面转场动画至关重要。它们不仅能提升用户体验,还能在视觉上引导用户,清晰地展示页面之间的逻辑关系。一个好的转场动画能够:

  • 增强应用的整体美观性
  • 提供更自然的导航体验
  • 减少用户因突兀切换产生的认知负担

Flutter 提供了多种方式来实现页面转场,其中 PageRouteBuilder 是最灵活、最强大的工具之一。

2. PageRouteBuilder:路由构建的基石

PageRouteBuilder 允许我们完全控制路由的构建过程,包括转场动画。它接收一个 pageBuilder 函数,用于构建目标页面,以及一个可选的 transitionsBuilder 函数,用于自定义转场动画。

基本用法:

Navigator.push(
  context,
  PageRouteBuilder(
    pageBuilder: (context, animation, secondaryAnimation) => const NextPage(),
    transitionsBuilder: (context, animation, secondaryAnimation, child) {
      // 自定义转场动画
      return child; // 默认直接显示目标页面
    },
  ),
);

class NextPage extends StatelessWidget {
  const NextPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Next Page')),
      body: const Center(child: Text('This is the next page')),
    );
  }
}

在这个例子中,pageBuilder 负责构建 NextPagetransitionsBuilder 则负责定义转场动画。目前,transitionsBuilder 只是简单地返回 child,这意味着没有实际的动画效果,直接显示目标页面。

3. 深入 transitionsBuilder:Animation 的魔力

transitionsBuilder 函数接收四个参数:

  • context: BuildContext
  • animation: 代表当前路由转场动画的 Animation 对象。通常是一个 Tween<double>,其值从 0.0 到 1.0 变化,表示动画的进度。
  • secondaryAnimation: 代表前一个路由转场动画的 Animation 对象。当当前路由被另一个路由覆盖时,这个动画会反向运行。
  • child: 即 pageBuilder 构建的页面 Widget。

我们可以利用 animation 对象来控制转场动画的各种属性,例如:

  • opacity: 透明度
  • transform: 变换(平移、旋转、缩放)
  • clipRect: 裁剪区域

示例:淡入淡出效果

Navigator.push(
  context,
  PageRouteBuilder(
    pageBuilder: (context, animation, secondaryAnimation) => const NextPage(),
    transitionsBuilder: (context, animation, secondaryAnimation, child) {
      return FadeTransition(
        opacity: animation,
        child: child,
      );
    },
  ),
);

FadeTransition 是一个预定义的 Widget,它根据 opacity 属性来控制透明度,从而实现淡入淡出效果。animation 对象的值从 0.0 到 1.0 变化,FadeTransition 会将 child 的透明度从 0 到 1 线性变化。

示例:滑动效果

Navigator.push(
  context,
  PageRouteBuilder(
    pageBuilder: (context, animation, secondaryAnimation) => const NextPage(),
    transitionsBuilder: (context, animation, secondaryAnimation, child) {
      const begin = Offset(1.0, 0.0); // 从右侧进入
      const end = Offset.zero;
      const curve = Curves.ease;

      var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve));

      var offsetAnimation = animation.drive(tween);

      return SlideTransition(
        position: offsetAnimation,
        child: child,
      );
    },
  ),
);

SlideTransition 也是一个预定义的 Widget,它根据 position 属性来控制位置,从而实现滑动效果。 我们首先定义了一个 Tween<Offset>,它描述了页面从哪里开始滑动(begin)到哪里结束(end)。 然后,我们使用 CurveTween 来应用一个缓动曲线,使滑动更加自然。 最后,我们使用 animation.drive() 方法将 animation 对象驱动 tween,得到一个 Animation<Offset> 对象,传递给 SlideTransition

参数解释:

参数 类型 描述
begin Offset 定义了动画开始时的偏移量。Offset(1.0, 0.0) 表示从屏幕右侧进入,Offset(-1.0, 0.0) 表示从屏幕左侧进入,Offset(0.0, 1.0) 表示从屏幕底部进入,Offset(0.0, -1.0) 表示从屏幕顶部进入。
end Offset 定义了动画结束时的偏移量。通常设置为 Offset.zero,表示页面滑动到屏幕中央。
curve Curve 定义了动画的缓动曲线。Flutter 提供了多种预定义的缓动曲线,例如 Curves.ease, Curves.easeInOut, Curves.linear 等。缓动曲线可以控制动画的速度变化,使其更加自然。
Tween Tween<Offset> Tween 定义了动画的起始值和结束值,以及动画类型。在这里,我们使用 Tween<Offset> 来定义一个 Offset 类型的动画。
CurveTween CurveTween CurveTween 允许我们将一个缓动曲线应用到 Tween 上。这可以使动画的速度变化更加自然。
animation.drive Animation animation.drive 方法将 animation 对象驱动 Tween,生成一个 Animation<Offset> 对象。这个 Animation<Offset> 对象的值会随着 animation 对象的值变化而变化,从而控制 SlideTransition 的位置。

4. Canvas 变换:终极的定制能力

Canvas 是 Flutter 中用于绘制 2D 图形的底层 API。通过 Canvas 变换,我们可以实现各种复杂的视觉效果,例如:

  • 旋转
  • 缩放
  • 倾斜
  • 透视

要使用 Canvas 变换,我们需要创建一个 CustomPainter,并在其 paint 方法中使用 Canvastranslate, rotate, scale, transform 等方法。

示例:旋转效果

class RotateTransition extends AnimatedWidget {
  const RotateTransition({
    Key? key,
    required Animation<double> animation,
    required this.child,
  }) : super(key: key, listenable: animation);

  final Widget child;

  @override
  Widget build(BuildContext context) {
    final Animation<double> animation = listenable as Animation<double>;
    return Transform.rotate(
      angle: animation.value * 2 * pi, // 旋转角度(弧度)
      child: child,
    );
  }
}

Navigator.push(
  context,
  PageRouteBuilder(
    pageBuilder: (context, animation, secondaryAnimation) => const NextPage(),
    transitionsBuilder: (context, animation, secondaryAnimation, child) {
      return RotateTransition(animation: animation, child: child);
    },
  ),
);

在这个例子中,我们创建了一个 RotateTransition Widget,它使用 Transform.rotate Widget 来旋转其 childTransform.rotateangle 属性指定了旋转角度,单位是弧度。 我们使用 animation.value * 2 * pi 来将 animation 对象的值映射到 0 到 2π 的范围内,从而实现一个完整的旋转。

直接使用 CustomPaint 和 Canvas 进行绘制

class CustomRotateTransition extends AnimatedWidget {
  const CustomRotateTransition({
    Key? key,
    required Animation<double> animation,
    required this.child,
  }) : super(key: key, listenable: animation);

  final Widget child;

  @override
  Widget build(BuildContext context) {
    final Animation<double> animation = listenable as Animation<double>;
    return CustomPaint(
      painter: _RotatePainter(animation.value),
      child: child,
    );
  }
}

class _RotatePainter extends CustomPainter {
  _RotatePainter(this.angle);

  final double angle;

  @override
  void paint(Canvas canvas, Size size) {
    canvas.save(); // 保存当前 Canvas 状态
    canvas.translate(size.width / 2, size.height / 2); // 将 Canvas 中心移动到 Widget 中心
    canvas.rotate(angle * 2 * pi); // 旋转 Canvas
    canvas.translate(-size.width / 2, -size.height / 2); // 将 Canvas 中心移回原位
    // 不在这里绘制任何东西,因为 child 会绘制自身
    canvas.restore(); // 恢复 Canvas 状态
  }

  @override
  bool shouldRepaint(_RotatePainter oldDelegate) {
    return oldDelegate.angle != angle;
  }
}

Navigator.push(
  context,
  PageRouteBuilder(
    pageBuilder: (context, animation, secondaryAnimation) => const NextPage(),
    transitionsBuilder: (context, animation, secondaryAnimation, child) {
      return CustomRotateTransition(animation: animation, child: child);
    },
  ),
);

在这个例子中,我们创建了一个 CustomRotateTransition Widget,它使用 CustomPaint Widget 和一个自定义的 _RotatePainter 来旋转其 child

_RotatePainterpaint 方法首先使用 canvas.save() 方法保存当前 Canvas 的状态。 然后,它使用 canvas.translate() 方法将 Canvas 的中心移动到 Widget 的中心。 接下来,它使用 canvas.rotate() 方法旋转 Canvas。 最后,它使用 canvas.translate() 方法将 Canvas 的中心移回原位,并使用 canvas.restore() 方法恢复 Canvas 的状态。

注意,我们没有在 paint 方法中绘制任何东西,因为 child 会绘制自身。 我们只需要旋转 Canvas,child 就会随着 Canvas 一起旋转。

shouldRepaint 方法用于判断是否需要重新绘制。 如果 angle 属性发生了变化,我们就需要重新绘制。

示例:缩放效果

class ScaleTransition extends AnimatedWidget {
  const ScaleTransition({
    Key? key,
    required Animation<double> animation,
    required this.child,
  }) : super(key: key, listenable: animation);

  final Widget child;

  @override
  Widget build(BuildContext context) {
    final Animation<double> animation = listenable as Animation<double>;
    return Transform.scale(
      scale: animation.value,
      child: child,
    );
  }
}

Navigator.push(
  context,
  PageRouteBuilder(
    pageBuilder: (context, animation, secondaryAnimation) => const NextPage(),
    transitionsBuilder: (context, animation, secondaryAnimation, child) {
      return ScaleTransition(animation: animation, child: child);
    },
  ),
);

在这个例子中,我们创建了一个 ScaleTransition Widget,它使用 Transform.scale Widget 来缩放其 childTransform.scalescale 属性指定了缩放比例。 我们使用 animation.value 来将 animation 对象的值直接作为缩放比例。

结合 Canvas 变换和预定义 Widget

我们可以将 Canvas 变换和预定义的 Widget 结合起来,创造出更复杂的转场动画。 例如,我们可以使用 ClipRect Widget 来裁剪页面,然后使用 Canvas 变换来旋转裁剪后的页面。

5. 性能优化

自定义路由转场动画可能会对性能产生影响,特别是当动画比较复杂时。为了优化性能,我们需要注意以下几点:

  • 避免不必要的重绘: CustomPaintershouldRepaint 方法应该尽可能精确地判断是否需要重新绘制。
  • 使用 RepaintBoundary RepaintBoundary Widget 可以将动画区域与其他区域隔离开,减少重绘范围。
  • 避免复杂的计算: 尽量使用简单的数学运算,避免复杂的计算,例如矩阵乘法。
  • 使用 cacheExtent 对于列表页面,可以设置 cacheExtent 属性,预先缓存一部分页面,提高滑动流畅度。

6. 一个更复杂的例子:立方体旋转效果

class CubeTransition extends AnimatedWidget {
  const CubeTransition({
    Key? key,
    required Animation<double> animation,
    required this.child,
    this.turns = 0.5,
  }) : super(key: key, listenable: animation);

  final Widget child;
  final double turns;

  @override
  Widget build(BuildContext context) {
    final Animation<double> animation = listenable as Animation<double>;
    final transform = Matrix4.identity();
    transform.setEntry(3, 2, 0.001); // 透视效果
    transform.rotateY(pi * turns * animation.value);
    return Transform(
      transform: transform,
      alignment: Alignment.center,
      child: child,
    );
  }
}

Navigator.push(
  context,
  PageRouteBuilder(
    pageBuilder: (context, animation, secondaryAnimation) => const NextPage(),
    transitionsBuilder: (context, animation, secondaryAnimation, child) {
      return CubeTransition(animation: animation, child: child);
    },
  ),
);

这个例子展示了一个简单的立方体旋转效果。 我们使用 Matrix4.identity() 创建一个单位矩阵,然后使用 transform.setEntry(3, 2, 0.001) 添加一个透视效果。 接下来,我们使用 transform.rotateY() 方法绕 Y 轴旋转矩阵。 最后,我们使用 Transform Widget 将矩阵应用到 child 上。

代码解释:

  • Matrix4.identity(): 创建一个 4×4 的单位矩阵。 单位矩阵是一种特殊的矩阵,它的对角线上的元素都是 1,其他元素都是 0。
  • transform.setEntry(3, 2, 0.001): 设置矩阵的第 3 行第 2 列的元素为 0.001。 这会添加一个透视效果,使旋转看起来更逼真。
  • transform.rotateY(pi * turns * animation.value): 绕 Y 轴旋转矩阵。 turns 属性指定旋转的圈数。 animation.value 的值从 0.0 到 1.0 变化,所以页面会旋转半圈。
  • Transform: 一个用于应用矩阵变换的 Widget。 alignment 属性指定旋转的中心点。

7. 结合 Hero 动画

Hero 动画可以平滑地将一个 Widget 从一个页面过渡到另一个页面。 我们可以将 Hero 动画和自定义路由转场动画结合起来,创造出更复杂的转场效果。

// 在源页面
Hero(
  tag: 'my-hero',
  child: Image.asset('assets/my_image.png'),
);

// 在目标页面
Hero(
  tag: 'my-hero',
  child: Image.asset('assets/my_image.png'),
);

Navigator.push(
  context,
  PageRouteBuilder(
    pageBuilder: (context, animation, secondaryAnimation) => const NextPage(),
    transitionsBuilder: (context, animation, secondaryAnimation, child) {
      return FadeTransition(opacity: animation, child: child); // 使用 FadeTransition 作为过渡
    },
  ),
);

在这个例子中,我们在源页面和目标页面都放置了一个 Hero Widget,它们的 tag 属性相同。 当我们从源页面导航到目标页面时,Flutter 会自动将源页面的 Hero Widget 平滑地过渡到目标页面的 Hero Widget。 我们同时使用 FadeTransition 来创建一个淡入淡出的过渡效果。

8. 总结

PageRouteBuilderCanvas 变换是 Flutter 中创建自定义路由转场动画的强大工具。 通过灵活运用这两个工具,我们可以创造出各种各样引人入胜的页面切换效果,提升应用的用户体验。 记住,在追求炫酷动画的同时,也要注意性能优化,确保应用的流畅运行。

精心设计的动画更具吸引力

通过深入学习 PageRouteBuilderCanvas 变换,我们能够创造出更具吸引力,更符合应用风格的页面转场动画。 这些动画不仅能提升用户体验,还能增强应用的品牌形象。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注