第5章:容器类组件 —— 5.3 变换(Transform)

39 阅读8分钟

5.3 变换(Transform)

📚 章节概览

本节学习 Transform 变换组件,它可以在绘制时对子组件应用矩阵变换:

  • Transform - 变换组件
  • Matrix4 - 4D变换矩阵
  • translate - 平移
  • rotate - 旋转
  • scale - 缩放
  • skew - 倾斜
  • Transform注意事项 - 绘制阶段vs布局阶段
  • RotatedBox - 布局阶段旋转
  • 组合变换 - 多种变换组合

🎯 核心知识点

Transform工作原理

┌─────────────────────────────────┐
│   Flutter渲染流程               │
├─────────────────────────────────┤
│ 1. Build(构建)                │
│ 2. Layout(布局)  ← RotatedBox │
│ 3. Paint(绘制)   ← Transform  │
└─────────────────────────────────┘

关键区别:

  • Transform:在绘制阶段应用变换,不影响布局
  • RotatedBox:在布局阶段应用旋转,影响布局

1️⃣ Transform(变换组件)

1.1 构造函数

Transform({
  Key? key,
  required Matrix4 transform,
  Offset? origin,
  AlignmentGeometry? alignment,
  bool transformHitTests = true,
  FilterQuality? filterQuality,
  Widget? child,
})

1.2 主要属性

属性类型说明
transformMatrix4变换矩阵(必需)
alignmentAlignmentGeometry?变换原点的对齐方式
originOffset?变换原点坐标
transformHitTestsbool是否变换触摸事件
childWidget?子组件

2️⃣ 平移(Translate)

2.1 Transform.translate

Transform.translate({
  Key? key,
  required Offset offset,
  bool transformHitTests = true,
  FilterQuality? filterQuality,
  Widget? child,
})

参数:

  • offset:平移偏移量(x, y)
    • 正值:向右/向下移动
    • 负值:向左/向上移动

2.2 示例

// 书中示例:左移20像素,上移5像素
DecoratedBox(
  decoration: BoxDecoration(color: Colors.red),
  child: Transform.translate(
    offset: Offset(-20.0, -5.0),
    child: Text("Hello world"),
  ),
)

效果:

原点 ─────────────────►
│                    
│  ┌────────────┐  ← 原始位置(红色背景)
│  │            │
│  └────────────┘
│
│ ┌────────────┐    ← 平移后位置(文字)
│ │ Hello world│
│ └────────────┘
▼

2.3 坐标系统

// 坐标原点在左上角
Offset(0, 0)      // 原点
Offset(10, 0)     // 向右10像素
Offset(-10, 0)    // 向左10像素
Offset(0, 10)     // 向下10像素
Offset(0, -10)    // 向上10像素

3️⃣ 旋转(Rotate)

3.1 Transform.rotate

Transform.rotate({
  Key? key,
  required double angle,
  Offset? origin,
  AlignmentGeometry? alignment = Alignment.center,
  bool transformHitTests = true,
  FilterQuality? filterQuality,
  Widget? child,
})

参数:

  • angle:旋转角度(单位:弧度
  • alignment:旋转中心(默认为组件中心)

3.2 弧度与角度转换

import 'dart:math' as math;

// 角度 → 弧度
double radians = degrees * math.pi / 180;

// 常用角度
math.pi / 2      // 90度
math.pi          // 180度
math.pi * 3 / 2  // 270度
math.pi * 2      // 360度

3.3 示例

// 书中示例:旋转90度
DecoratedBox(
  decoration: BoxDecoration(color: Colors.red),
  child: Transform.rotate(
    angle: math.pi / 2,  // 旋转90度
    child: Text("Hello world"),
  ),
)

⚠️ 注意: 需要导入 dart:math

import 'dart:math' as math;

4️⃣ 缩放(Scale)

4.1 Transform.scale

Transform.scale({
  Key? key,
  double? scale,
  double? scaleX,
  double? scaleY,
  Offset? origin,
  AlignmentGeometry? alignment = Alignment.center,
  bool transformHitTests = true,
  FilterQuality? filterQuality,
  Widget? child,
})

参数:

  • scale:统一缩放比例
  • scaleX:X轴缩放比例
  • scaleY:Y轴缩放比例

4.2 缩放值说明

效果
< 1.0缩小
1.0不变
> 1.0放大
0.5缩小到一半
2.0放大到两倍

4.3 示例

// 书中示例:放大到1.5倍
DecoratedBox(
  decoration: BoxDecoration(color: Colors.red),
  child: Transform.scale(
    scale: 1.5,
    child: Text("Hello world"),
  ),
)

// 分别设置X、Y轴缩放
Transform.scale(
  scaleX: 2.0,  // X轴放大2倍
  scaleY: 0.5,  // Y轴缩小到一半
  child: Text("Hello world"),
)

5️⃣ 倾斜(Skew)

5.1 Matrix4.skew

倾斜变换需要使用 Matrix4

// 沿X轴倾斜
Matrix4.skewX(double alpha)

// 沿Y轴倾斜
Matrix4.skewY(double alpha)

// 同时倾斜X、Y轴
Matrix4.skew(double alpha, double beta)

参数: alpha/beta 为倾斜角度(单位:弧度)

5.2 示例

// 书中示例:沿Y轴倾斜0.3弧度
Container(
  color: Colors.black,
  child: Transform(
    alignment: Alignment.topRight,
    transform: Matrix4.skewY(0.3),
    child: Container(
      padding: EdgeInsets.all(8.0),
      color: Colors.deepOrange,
      child: Text('Apartment for rent!'),
    ),
  ),
)

效果:

┌─────────────┐        ┌──────────────┐
│  原始形状    │   →   ╱  倾斜后形状   ╱
└─────────────┘       └──────────────┘

6️⃣ Transform注意事项

6.1 不影响布局空间

重要: Transform 只在绘制阶段应用,不改变组件的布局空间和位置!

示例:文字重叠问题
// 书中示例:缩放后文字重叠
Row(
  mainAxisAlignment: MainAxisAlignment.center,
  children: [
    DecoratedBox(
      decoration: BoxDecoration(color: Colors.red),
      child: Transform.scale(
        scale: 1.5,  // 放大1.5倍
        child: Text("Hello world"),
      ),
    ),
    Text("你好", style: TextStyle(color: Colors.green, fontSize: 18.0)),
  ],
)

问题分析:

┌─────────────────────────────┐
│ 布局空间(红色,未变化)     │
│ ┌──────┐                    │
│ │Hello │  ← 实际布局空间     │
│ └──────┘                    │
│                             │
│   ┌────────────┐            │
│   │Hello world │  ← 绘制时放大 │
│   └────────────┘            │
│              你好  ← 紧挨红色区域 │
└─────────────────────────────┘
     ↑
   文字重叠!

6.2 性能优势

由于 Transform 只作用于绘制阶段:

  • ✅ 不需要重新布局(layout)
  • ✅ 不需要重新构建(build)
  • ✅ 性能好,适合动画
  • ✅ Flutter动画组件大量使用

应用场景:

  • 动画效果
  • 视觉调整
  • 性能敏感的场景

7️⃣ RotatedBox vs Transform.rotate

7.1 核心区别

特性Transform.rotateRotatedBox
作用阶段绘制阶段(Paint)布局阶段(Layout)
影响布局❌ 不影响✅ 影响
占用空间原始大小旋转后大小
旋转角度任意角度90度的倍数
参数单位弧度四分之一圈

7.2 Transform.rotate示例

// 书中示例
Row(
  mainAxisAlignment: MainAxisAlignment.center,
  children: [
    DecoratedBox(
      decoration: BoxDecoration(color: Colors.red),
      child: Transform.rotate(
        angle: math.pi / 2,  // 旋转90度
        child: Text("Hello world"),
      ),
    ),
    Text("你好", style: TextStyle(color: Colors.green, fontSize: 18.0)),
  ],
)

效果:

  • 红色区域:原始布局空间(未旋转)
  • 文字:绘制时旋转90度
  • 可能与"你好"重叠

7.3 RotatedBox示例

// 书中示例(改用RotatedBox)
Row(
  mainAxisAlignment: MainAxisAlignment.center,
  children: [
    DecoratedBox(
      decoration: BoxDecoration(color: Colors.red),
      child: RotatedBox(
        quarterTurns: 1,  // 旋转90度(1/4圈)
        child: Text("Hello world"),
      ),
    ),
    Text("你好", style: TextStyle(color: Colors.green, fontSize: 18.0)),
  ],
)

效果:

  • 红色区域:旋转后的实际布局空间
  • 子组件:旋转90度
  • 不会重叠

7.4 RotatedBox参数

RotatedBox({
  Key? key,
  required int quarterTurns,  // 旋转的1/4圈数
  Widget? child,
})

quarterTurns值:

  • 0:不旋转
  • 1:旋转90度(顺时针)
  • 2:旋转180度
  • 3:旋转270度
  • 4:旋转360度(回到原位)
  • -1:旋转-90度(逆时针)

8️⃣ 组合变换

8.1 思考题:顺序很重要!

问题: 先平移后旋转 vs 先旋转后平移,效果一样吗?

答案:不一样!

先平移后旋转
Transform.rotate(
  angle: math.pi / 4,
  child: Transform.translate(
    offset: Offset(30, 0),
    child: Container(...),
  ),
)

过程:

  1. 在原坐标系上向右平移30像素
  2. 然后旋转45度(连同坐标系一起旋转)
先旋转后平移
Transform.translate(
  offset: Offset(30, 0),
  child: Transform.rotate(
    angle: math.pi / 4,
    child: Container(...),
  ),
)

过程:

  1. 先旋转45度(坐标系也旋转)
  2. 在新坐标系上向右平移30像素

图解:

原坐标系:
    Y
    ↑
    │
    └───→ X

先平移后旋转:
1. 平移 → (30, 0)
2. 旋转 → 绕原点旋转

先旋转后平移:
1. 旋转 → 坐标系旋转45°
2. 平移 → 沿新X轴平移

8.2 使用Matrix4组合

Transform(
  transform: Matrix4.identity()
    ..translate(10.0, 20.0)  // 平移
    ..rotateZ(0.5)           // 旋转
    ..scale(1.5),            // 缩放
  child: Container(...),
)

链式调用:

  • .. 是Dart的级联操作符
  • 按顺序应用变换

9️⃣ 3D变换

9.1 Matrix4支持3D变换

// 绕X轴旋转
Matrix4.identity()..rotateX(angle)

// 绕Y轴旋转
Matrix4.identity()..rotateY(angle)

// 绕Z轴旋转
Matrix4.identity()..rotateZ(angle)

9.2 透视效果

Transform(
  transform: Matrix4.identity()
    ..setEntry(3, 2, 0.001)  // 设置透视
    ..rotateX(0.6),          // X轴旋转
  alignment: Alignment.center,
  child: Container(
    width: 150,
    height: 100,
    decoration: BoxDecoration(
      gradient: LinearGradient(
        colors: [Colors.orange, Colors.red],
      ),
    ),
    child: Center(child: Text('3D效果')),
  ),
)

setEntry参数:

  • setEntry(3, 2, value):设置透视值
  • 值越大,透视效果越明显
  • 典型值:0.0010.003

🤔 常见问题(FAQ)

Q1: Transform和Container.transform有什么区别?

A: 功能相同,只是用法不同

// 方式1:Transform组件
Transform.rotate(
  angle: math.pi / 4,
  child: Container(...),
)

// 方式2:Container.transform
Container(
  transform: Matrix4.rotationZ(math.pi / 4),
  child: ...,
)

Q2: 为什么Transform不影响布局?

A: 因为它在绘制阶段执行,而布局已经完成

Flutter渲染流程:
1. Build(构建Widget树)
2. Layout(计算大小和位置)  ← 布局完成
3. Paint(绘制)              ← Transform在这里执行

Q3: 如何让Transform影响布局?

A: 有以下方法:

  1. 使用RotatedBox(仅旋转)
RotatedBox(
  quarterTurns: 1,
  child: Text("Hello"),
)
  1. 使用LayoutBuilder动态计算
LayoutBuilder(
  builder: (context, constraints) {
    return Container(
      width: constraints.maxWidth * 1.5,  // 手动调整布局
      child: Transform.scale(
        scale: 1.5,
        child: Text("Hello"),
      ),
    );
  },
)

Q4: 如何实现卡片翻转动画?

A: 使用AnimationController + Transform

class FlipCard extends StatefulWidget {
  @override
  _FlipCardState createState() => _FlipCardState();
}

class _FlipCardState extends State<FlipCard>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  
  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: Duration(milliseconds: 500),
      vsync: this,
    );
  }
  
  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      builder: (context, child) {
        return Transform(
          transform: Matrix4.identity()
            ..setEntry(3, 2, 0.001)  // 透视
            ..rotateY(_controller.value * math.pi),  // 旋转
          alignment: Alignment.center,
          child: Container(
            width: 200,
            height: 300,
            color: Colors.blue,
            child: Center(child: Text('卡片')),
          ),
        );
      },
    );
  }
}

Q5: Transform会影响触摸事件吗?

A: 默认会,可以通过 transformHitTests 控制

Transform.scale(
  scale: 2.0,
  transformHitTests: true,  // 默认:变换触摸事件
  child: GestureDetector(
    onTap: () => print('Tapped'),
    child: Container(
      width: 50,
      height: 50,
      color: Colors.red,
    ),
  ),
)

// transformHitTests: false
// 触摸事件不会随Transform变换

🎯 跟着做练习

练习1:实现旋转动画按钮

目标: 点击按钮时360度旋转

要求:

  • 使用Transform.rotate
  • AnimationController控制动画
  • 点击触发旋转
💡 查看答案
class RotatingButton extends StatefulWidget {
  @override
  _RotatingButtonState createState() => _RotatingButtonState();
}

class _RotatingButtonState extends State<RotatingButton>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  
  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: Duration(milliseconds: 500),
      vsync: this,
    );
  }
  
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
  
  void _rotate() {
    _controller.reset();
    _controller.forward();
  }
  
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: _rotate,
      child: AnimatedBuilder(
        animation: _controller,
        builder: (context, child) {
          return Transform.rotate(
            angle: _controller.value * 2 * math.pi,
            child: Container(
              padding: EdgeInsets.symmetric(
                horizontal: 24,
                vertical: 12,
              ),
              decoration: BoxDecoration(
                gradient: LinearGradient(
                  colors: [Colors.blue, Colors.purple],
                ),
                borderRadius: BorderRadius.circular(8),
              ),
              child: Text(
                '点击旋转',
                style: TextStyle(color: Colors.white),
              ),
            ),
          );
        },
      ),
    );
  }
}

练习2:实现脉冲动画

目标: 组件循环放大缩小

要求:

  • 使用Transform.scale
  • 循环动画
  • 从1.0到1.2再回到1.0
💡 查看答案
class PulseAnimation extends StatefulWidget {
  final Widget child;
  
  const PulseAnimation({Key? key, required this.child}) : super(key: key);
  
  @override
  _PulseAnimationState createState() => _PulseAnimationState();
}

class _PulseAnimationState extends State<PulseAnimation>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation;
  
  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: Duration(milliseconds: 1000),
      vsync: this,
    )..repeat(reverse: true);  // 循环并反向
    
    _animation = Tween<double>(
      begin: 1.0,
      end: 1.2,
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Curves.easeInOut,
    ));
  }
  
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
  
  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _animation,
      builder: (context, child) {
        return Transform.scale(
          scale: _animation.value,
          child: widget.child,
        );
      },
    );
  }
}

// 使用
PulseAnimation(
  child: Icon(
    Icons.favorite,
    size: 50,
    color: Colors.red,
  ),
)

练习3:实现视差滚动效果

目标: 根据滚动位置应用不同的Transform

要求:

  • 使用ListView
  • Transform.translate实现视差
  • 不同层级移动速度不同
💡 查看答案
class ParallaxScroll extends StatefulWidget {
  @override
  _ParallaxScrollState createState() => _ParallaxScrollState();
}

class _ParallaxScrollState extends State<ParallaxScroll> {
  final ScrollController _controller = ScrollController();
  double _offset = 0;
  
  @override
  void initState() {
    super.initState();
    _controller.addListener(() {
      setState(() {
        _offset = _controller.offset;
      });
    });
  }
  
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
  
  @override
  Widget build(BuildContext context) {
    return ListView(
      controller: _controller,
      children: [
        // 背景层(移动慢)
        Transform.translate(
          offset: Offset(0, _offset * 0.3),
          child: Container(
            height: 300,
            color: Colors.blue[100],
            child: Center(child: Text('背景层')),
          ),
        ),
        // 中间层(正常速度)
        Container(
          height: 300,
          color: Colors.green[100],
          child: Center(child: Text('中间层')),
        ),
        // 前景层(移动快)
        Transform.translate(
          offset: Offset(0, -_offset * 0.3),
          child: Container(
            height: 300,
            color: Colors.orange[100],
            child: Center(child: Text('前景层')),
          ),
        ),
        // 其他内容
        ...List.generate(10, (index) {
          return Container(
            height: 100,
            color: index.isEven ? Colors.grey[200] : Colors.grey[300],
            child: Center(child: Text('项目 $index')),
          );
        }),
      ],
    );
  }
}

📊 Transform方法速查表

方法参数说明示例
Transform.translateoffset平移Offset(10, 20)
Transform.rotateangle旋转math.pi / 2
Transform.scalescale缩放1.5
Transform.scalescaleX, scaleY分轴缩放scaleX: 2.0
Matrix4.skewXalphaX轴倾斜0.3
Matrix4.skewYalphaY轴倾斜0.3
Matrix4.rotateXangle绕X轴旋转0.5
Matrix4.rotateYangle绕Y轴旋转0.5
Matrix4.rotateZangle绕Z轴旋转0.5

📋 小结

核心概念

// Transform:绘制阶段变换,不影响布局
Transform.translate(offset: Offset(10, 0), child: ...)
Transform.rotate(angle: math.pi / 4, child: ...)
Transform.scale(scale: 1.5, child: ...)
Transform(transform: Matrix4.skewX(0.3), child: ...)

// RotatedBox:布局阶段旋转,影响布局
RotatedBox(quarterTurns: 1, child: ...)

// 组合变换(顺序很重要!)
Transform(
  transform: Matrix4.identity()
    ..translate(10.0)
    ..rotate(0.5)
    ..scale(1.5),
  child: ...,
)

记忆技巧

  1. Transform = 绘制阶段 = 不影响布局 = 性能好
  2. RotatedBox = 布局阶段 = 影响布局 = 90度倍数
  3. 变换顺序 = 先平移后旋转 ≠ 先旋转后平移
  4. 弧度转换 = 角度 × π / 180

🔗 相关资源