如何在 Flutter 中使用自定义动画和剪裁(clipping)实现一个简单的动画效果。
前置知识点学习
AnimationController
`AnimationController` 是 Flutter 动画框架中的一个核心类,用于控制动画的生命周期和状态。它提供了一种灵活的方式来定义动画的开始、结束、暂停、反向和速度调节等功能。
主要属性
- `duration`: 定义动画的时长。可以是 `Duration` 类型的值,如 `Duration(milliseconds: 500)`。
- `vsync`: 一个 `TickerProvider`,用于防止动画在不需要时消耗资源。通常在 `State` 类中通过 `SingleTickerProviderStateMixin` 提供。
- `value`: 表示动画当前的进度,范围通常是 0.0 到 1.0。
- `lowerBound` 和 `upperBound`: 定义动画值的范围,默认是 0.0 到 1.0。
主要方法
- `forward()`: 正向播放动画,从当前值到 `upperBound`。
- `reverse()`: 反向播放动画,从当前值到 `lowerBound`。
- `repeat()`: 循环播放动画,可以设置次数和是否反向。
- `stop()`: 停止动画。
- `reset()`: 将动画值重置为 `lowerBound`。
- `dispose()`: 销毁控制器,释放资源。在 `State` 的 `dispose` 方法中调用。
监听器
- `addListener()`: 添加一个回调函数,每当动画的值改变时调用。
- `addStatusListener()`: 添加一个回调函数,每当动画的状态改变时调用,比如开始、结束、正向播放、反向播放等。
使用示例
以下是一个简单的例子,演示如何使用 `AnimationController` 创建一个简单的透明度动画:
import 'package:flutter/material.dart';
class MyAnimationControllerExample extends StatefulWidget {
const MyAnimationControllerExample({super.key});
@override
_MyAnimationControllerExampleState createState() {
return _MyAnimationControllerExampleState();
}
}
class _MyAnimationControllerExampleState
extends State<MyAnimationControllerExample>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller =
AnimationController(vsync: this, duration: const Duration(seconds: 2));
_controller.addListener(() {
setState(() {});
});
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('AnimationController Example')),
body: Center(
child: Opacity(
opacity: _controller.value,
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
),
),
);
}
}
解释
- `AnimationController`: 控制动画的时长和进度。
- `SingleTickerProviderStateMixin`: 为 `vsync` 提供 `TickerProvider`,防止不必要的资源消耗。
- `addListener`: 在动画值改变时更新 UI。
- `forward`: 使动画从 `lowerBound` 开始到 `upperBound` 结束。
FloatingActionButton
`FloatingActionButton`(FAB)是 Flutter 中一个用于执行主操作的圆形按钮。它通常悬浮在应用界面的某个位置,用户可以通过点击它来触发特定的操作或功能。FAB 是 Material Design 的一部分,常见于各种应用中,用于吸引用户注意并方便地进行交互。
关键属性
- `child`: 该属性用于指定按钮内部的内容,通常是一个图标(`Icon`)或文本(`Text`)。这个内容会在按钮的中心显示。
- `onPressed`: 一个回调函数,当用户点击按钮时会被调用。这个属性是必需的,因为它定义了按钮的行为。
- `tooltip`: 当用户长按按钮时显示的提示文本,通常用于描述按钮的功能。
- `backgroundColor`: 按钮的背景颜色。
- `foregroundColor`: 按钮内容(如图标或文本)的颜色。
- `elevation`: 按钮的阴影深度,影响按钮的浮动效果。
- `shape`: 定义按钮的形状,默认是圆形,也可以自定义为其他形状。
- `heroTag`: 用于在页面切换时标识 FAB 的唯一标识符,默认提供避免动画冲突。
使用示例
下面是一个使用 `FloatingActionButton` 的简单示例:
import 'package:flutter/material.dart';
class FloatingActionButtonExample extends StatelessWidget {
const FloatingActionButtonExample({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('FloatingActionButton Example'),
),
body: const Center(
child: Text("Press the button below!"),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
print("FAB clicked!");
},
tooltip: 'Increment',
child: const Icon(Icons.add),
),
floatingActionButtonLocation: FloatingActionButtonLocation.endDocked);
}
}
解释
- `Scaffold`: Flutter 提供的一个布局结构,支持 Material Design 的组件,包括 FAB。
- `floatingActionButton`: `Scaffold` 的一个属性,用于指定屏幕上的 FAB。
- `onPressed`: 定义当 FAB 被点击时的行为。在这个例子中,它只是打印一条消息。
- `Icon`: 在 FAB 中展示的内容,在这个例子中是一个加号图标。
- `floatingActionButtonLocation`: 用于定义 FAB 在屏幕中的位置,如居中、靠右或靠左等。
常见使用场景
- 主要操作: FAB 通常用于执行应用的主要操作,如在邮件应用中创建新邮件、在社交应用中发布新内容等。
- 辅助功能: 在一些应用中,FAB 可以用于快速访问某些辅助功能。
- 动态操作: 在某些应用中,FAB 的功能可能会根据上下文动态变化,比如在不同的页面中执行不同的操作。
通过 `FloatingActionButton`,开发者可以在 Flutter 应用中轻松实现符合 Material Design 指导原则的交互元素。它是一个非常直观且易于使用的组件,用于增强用户体验。
CustomClipper
`CustomClipper` 是 Flutter 提供的一个抽象类,用于创建自定义剪裁(clipping)效果。通过实现 `CustomClipper`,你可以定义任意形状的剪裁路径,应用于组件的外观。
主要方法
`CustomClipper` 包含两个主要方法,你需要在子类中实现它们:
1.`getClip(Size size)`:
- 返回一个 `Path` 对象,该对象定义了应该如何剪裁组件。
- `Size` 参数提供了组件的大小,你可以根据这个大小来计算剪裁路径。
2.`shouldReclip(CustomClipper oldClipper)`:
- 返回一个布尔值,决定是否需要重新剪裁。当剪裁路径依赖于某些动态变化的参数时,你需要在这个方法中进行判断。
- 通常,如果你的剪裁路径是固定不变的,可以返回 `false`。
使用方法
1.创建一个 `CustomClipper` 子类:
- 实现 `getClip` 方法来定义剪裁路径。
- 实现 `shouldReclip` 方法来决定何时重新剪裁。
2.使用 `ClipPath` 或其他 `Clip*` 组件:
- 将自定义 `CustomClipper` 实例传递给 `ClipPath`、`ClipRect`、`ClipOval` 等组件的 `clipper` 属性。
示例
以下是一个简单的示例,展示如何使用 `CustomClipper` 来创建一个三角形剪裁效果:
import 'package:flutter/material.dart';
class TriangleClipper extends CustomClipper<Path> {
@override
Path getClip(Size size) {
final path = Path();
path.moveTo(size.width / 2, 0);
path.lineTo(size.width, size.height);
path.lineTo(0, size.height);
path.close();
return path;
}
@override
bool shouldReclip(covariant CustomClipper<Path> oldClipper) {
// 如果路径不依赖外部状态,可以返回 false
return false;
}
}
class CustomClipperExample extends StatelessWidget {
const CustomClipperExample({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('CustomClipper Example'),
),
body: Center(
child: ClipPath(
clipper: TriangleClipper(), // 使用自定义的 TriangleClipper
child: Container(
width: 200,
height: 200,
color: Colors.blue,
),
),
),
);
}
}
解释
- TriangleClipper`: 自定义的剪裁器,实现了一个简单的三角形路径。
- `getClip` 方法: 定义了一个三角形的路径。
- `ClipPath`: 使用 `TriangleClipper` 将子组件剪裁成三角形。
使用场景
- 自定义形状: 当你需要超出标准形状的剪裁效果时,比如特定的波浪形、星形等。
- 动态剪裁: 如果剪裁形状需要根据某些动态参数变化,可以通过 `shouldReclip` 来控制重新剪裁。
- 视觉效果: 增强 UI 的视觉效果,通过不规则的形状吸引用户注意力。
lerpDouble函数解析
在 Flutter 中,`lerpDouble` 是一个用于在两个 `double` 值之间进行线性插值的方法。它通常用于动画和其他需要平滑过渡的场景。
主要功能
`lerpDouble` 的主要功能是根据给定的插值因子 `t`,计算出两个 `double` 值之间的中间值。这个过程称为线性插值(linear interpolation),简称 lerp。
方法签名
double? lerpDouble(
num? a,
num? b,
double t,
)
参数
- `a`: 起始值,可以是 `double` 或 `null`。如果为 `null`,则在计算时视为 0.0。
- `b`: 结束值,可以是 `double` 或 `null`。如果为 `null`,则在计算时视为 0.0。
- `t`: 插值因子,是一个介于 0.0 到 1.0 之间的 `double`。当 `t` 为 0.0 时,返回 `a`;当 `t` 为 1.0 时,返回 `b`;在这之间返回 `a` 和 `b` 的插值。
返回值
返回一个 `double` 类型的值,表示 `a` 和 `b` 之间的插值。如果 `a` 和 `b` 都为 `null`,则返回 `null`。
用法示例
以下是一个简单的示例,展示如何使用 `lerpDouble` 计算两个值之间的插值:
import 'dart:ui';
void main() {
double? start = 10.0;
double? end = 20.0;
double t = 0.25; // 插值因子
double? interpolatedValue = lerpDouble(start, end, t);
print('Interpolated Value: $interpolatedValue'); // 输出: Interpolated Value: 12.5
}
解释
- 在上面的例子中,`start` 是 10.0,`end` 是 20.0,`t` 是 0.25。
- `lerpDouble` 返回两个值之间的 25% 位置上的值,即 12.5。
使用场景
- 动画: 在动画过程中,计算属性的中间值,比如位置、大小、透明度等。
- 过渡效果: 在不同状态之间平滑过渡,例如颜色渐变、尺寸变化等。
- 自定义插值: 在需要自定义插值逻辑的情况下,用于计算中间值。
`lerpDouble` 是一个简单却强大的工具,允许开发者在两个数值之间创建平滑的过渡效果,非常适合用于动画和动态 UI 变化中。
Path
在 Flutter 中,`Path` 是一个用于定义向量形状的类。它允许开发者创建复杂的几何图形,通过一系列的直线和曲线来定义路径。`Path` 类可以用于绘制形状、创建剪裁区域以及生成自定义绘制效果。
基本用法
`Path` 提供了一系列方法,用于定义形状的边界。以下是一些常用的方法:
- `moveTo(double x, double y)`: 移动当前点到指定的坐标,开始新的子路径。
- `lineTo(double x, double y)`: 从当前点绘制一条直线到指定的坐标。
- `arcTo(Rect rect, double startAngle, double sweepAngle, bool forceMoveTo)`: 绘制一个圆弧,基于一个矩形的边界。
- `quadraticBezierTo(double x1, double y1, double x2, double y2)`: 绘制一个二次贝塞尔曲线。
- `cubicTo(double x1, double y1, double x2, double y2, double x3, double y3)`: 绘制一个三次贝塞尔曲线。
- `close()`: 关闭当前子路径,连接最后一个点到第一个点,形成一个封闭的形状。
示例
下面是一个简单的示例,使用 `Path` 绘制一个三角形:
import 'package:flutter/material.dart';
class PathExampleDemo extends StatelessWidget {
const PathExampleDemo({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Path Example'),
),
body: Center(
child: CustomPaint(
size: const Size(200, 200),
painter: TrianglePainter(),
),
),
);
}
}
class TrianglePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.blue
..style = PaintingStyle.fill;
final path = Path();
//顶点
path.moveTo(size.width / 2, 0);
//右下角
path.lineTo(size.width, size.height);
//左下角
path.lineTo(0, size.height);
path.close();
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return false;
}
}
解释
- `CustomPainter`: 用于自定义绘制。`paint` 方法中使用 `Canvas` 对象进行绘制。
- `Path`: 定义路径的形状。在这个例子中,绘制了一个简单的三角形。
- `Canvas.drawPath`: 使用 `Path` 和 `Paint` 对象在画布上绘制路径。
使用场景
- 自定义形状: `Path` 可以定义任意形状,用于自定义绘制或剪裁。
- 复杂图形: 使用贝塞尔曲线和弧形,可以创建复杂的图形和路径。
- 动画路径: 在动画中,可以使用 `Path` 来定义对象的运动轨迹。
注意事项
- 路径方向: 在定义路径时,方向(顺时针或逆时针)可能会影响填充规则。
- 性能: 复杂路径可能会影响性能,尤其是在动画中,请合理使用。
AnimatedBuilder
`AnimatedBuilder` 是 Flutter 动画框架中的一个小部件,用于将动画与 UI 组件进行绑定。它提供了一种高效的方法来重建与动画相关的部分 UI,而无需重建整个 widget 树。
核心概念
`AnimatedBuilder` 通过监听一个 `Listenable` 对象(通常是 `AnimationController` 或其他 `Animation` 对象)来决定何时重建 UI。当动画对象的值发生变化时,`AnimatedBuilder` 会调用其构建方法,从而更新与动画相关的 UI。
主要属性
- `animation`: 一个 `Listenable` 对象,通常是 `Animation` 或 `AnimationController`。`AnimatedBuilder` 监听这个对象的变化。
- `builder`: 一个回调函数,接受两个参数:`BuildContext` 和 `Widget`。在这个函数中,你可以根据动画的当前状态来构建和返回一个新的 widget 树。
- `child`: 一个可选的小部件,当它在动画变化时不需要重建时,可以作为优化传递给 `builder`。这样可以避免不必要的重建。
使用示例
以下是一个使用 `AnimatedBuilder` 的简单示例,展示如何创建一个旋转动画:
import 'package:flutter/material.dart';
class AnimatedBuilderExample extends StatefulWidget {
const AnimatedBuilderExample({super.key});
@override
_AnimatedBuilderExampleState createState() {
return _AnimatedBuilderExampleState();
}
}
class _AnimatedBuilderExampleState extends State<AnimatedBuilderExample>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('AnimatedBuilder Example')),
body: Center(
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform.rotate(
angle: _controller.value * 2.0 * 3.14,
child: child,
);
},
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
),
),
);
}
@override
void initState() {
super.initState();
_controller =
AnimationController(vsync: this, duration: const Duration(seconds: 2))
..repeat(); //无限循环动画
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
解释
- `AnimationController`: 控制动画的时长和进度。在这个例子中,`_controller` 在 2 秒内从 0.0 到 1.0 循环。
- `AnimatedBuilder`: 监听 `_controller` 的变化,并在 `builder` 回调中根据动画的当前值更新 UI。
- `Transform.rotate`: 根据动画的当前值旋转 `child`,实现旋转效果。
- `child`: 传递给 `AnimatedBuilder` 的 `child` 是一个蓝色的方块,它在动画期间不会重建。
使用场景
- 动画优化: 当只有部分 UI 需要随着动画更新时,`AnimatedBuilder` 可以避免整个 widget 树的重建。
- 复杂动画: 在需要多个动画组合或复杂动画效果时,`AnimatedBuilder` 提供了一种灵活的方式来管理和应用这些动画。
自定义动画代码学习
import 'dart:math';
import 'dart:ui';
import 'package:flutter/material.dart';
class AnimaDemoPage22 extends StatefulWidget {
const AnimaDemoPage22({super.key});
@override
_AnimaDemoPageState22 createState() {
return _AnimaDemoPageState22();
}
}
class _AnimaDemoPageState22 extends State<AnimaDemoPage22>
with SingleTickerProviderStateMixin {
late AnimationController controller;
Animation? animation;
@override
void initState() {
super.initState();
controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 500),
);
animation = CurvedAnimation(parent: controller, curve: Curves.easeInSine);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("AnimaDemoPage22"),
),
body: Container(
color: Colors.blueGrey,
child: MyCRAnimation(
minR: 0,
maxR: 250,
offset: Offset(MediaQuery.sizeOf(context).width / 2,
MediaQuery.sizeOf(context).height / 2),
animation: animation as Animation<double>?,
child: Center(
child: Container(
alignment: Alignment.center,
height: 250,
width: 250,
color: Colors.greenAccent,
child: const Text("动画测试"),
),
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
if (controller.status == AnimationStatus.completed ||
controller.status == AnimationStatus.forward) {
controller.reverse();
} else {
controller.forward();
}
},
child: const Text("点击"),
),
);
}
}
class MyCRAnimation extends StatelessWidget {
final Offset? offset;
final double? minR;
final double? maxR;
final Widget child;
final Animation<double>? animation;
const MyCRAnimation(
{super.key,
required this.child,
required this.animation,
this.offset,
this.minR,
this.maxR});
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: animation!,
builder: (_, __) {
return ClipPath(
clipper: MyAnimationClipper(
value: animation!.value,
minR: minR,
maxR: maxR,
offset: offset),
child: child,
);
});
}
}
class MyAnimationClipper extends CustomClipper<Path> {
final double? value;
final double? minR;
final double? maxR;
final Offset? offset;
MyAnimationClipper({this.value, this.offset, this.minR, this.maxR});
@override
Path getClip(Size size) {
var path = Path();
var offset = this.offset ?? Offset(size.width / 2, size.height / 2);
var maxRadius = minR ?? radiusSize(size, offset);
var minRadius = maxR ?? 0;
var radius = lerpDouble(minRadius, maxRadius, value!)!;
var rect = Rect.fromCircle(center: offset, radius: radius);
path.addOval(rect);
return path;
}
@override
bool shouldReclip(covariant CustomClipper<Path> oldClipper) {
return true;
}
double radiusSize(Size size, Offset offset) {
final height = max(offset.dy, size.height - offset.dy);
final width = max(offset.dx, size.width - offset.dx);
return sqrt(width * width + height * height);
}
}