Flutter如何实现带动画且触发子组件重布局的旋转效果?
核心原理
RotatedBox 会在布局阶段处理旋转逻辑,会根据旋转角度重新计算子组件的尺寸约束,触发子组件重布局,但它的参数quarterTurns仅支持整数类型(对应90度的整数倍旋转),原生不支持连续动画效果;而Transform.rotate仅在绘制阶段做变换,不会修改布局约束,因此不会触发子组件重排。
方案1:90度整数倍旋转场景(适配RotatedBox的使用场景)
如果你的旋转需求仅为90/180/270度的整数倍,可自定义隐式动画组件AnimatedRotatedBox,内部同时处理布局切换和过渡动画,既保留RotatedBox的重布局能力,又能实现平滑旋转效果:
import 'package:flutter/material.dart'; import 'dart:math'; class AnimatedRotatedBox extends ImplicitlyAnimatedWidget { final int quarterTurns; final Widget child; const AnimatedRotatedBox({ super.key, required this.quarterTurns, required this.child, required super.duration, super.curve = Curves.linear, }); @override ImplicitlyAnimatedWidgetState<AnimatedRotatedBox> createState() => _AnimatedRotatedBoxState(); } class _AnimatedRotatedBoxState extends ImplicitlyAnimatedWidgetState<AnimatedRotatedBox> { IntTween? _quarterTurnsTween; late Animation<double> _angleAnimation; @override void forEachTween(TweenVisitor<dynamic> visitor) { _quarterTurnsTween = visitor( _quarterTurnsTween, widget.quarterTurns, (dynamic value) => IntTween(begin: value as int), ) as IntTween?; } @override void didUpdateTweens() { _angleAnimation = animation.drive(Tween<double>( begin: _quarterTurnsTween!.begin! * pi / 2, end: _quarterTurnsTween!.end! * pi / 2, )); } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: _angleAnimation, builder: (context, child) { // 动画结束后使用RotatedBox保证布局正确,动画过程中用Transform做过渡 final isAnimating = _angleAnimation.status != AnimationStatus.completed; if (isAnimating) { return Transform.rotate( angle: _angleAnimation.value, child: child, ); } return RotatedBox( quarterTurns: widget.quarterTurns, child: child!, ); }, child: widget.child, ); } }
使用示例:
// 点击触发旋转+重布局动画 bool _isRotated = false; @override Widget build(BuildContext context) { return GestureDetector( onTap: () => setState(() => _isRotated = !_isRotated), child: AnimatedRotatedBox( quarterTurns: _isRotated ? 1 : 0, duration: const Duration(milliseconds: 500), child: Stack( children: [ Image.network('https://i.imgur.com/Xl3LjXZ.jpeg'), Image.network('https://i.imgur.com/tF3g1JA.jpeg', width: 30, height: 30), ], ), ), ); }
方案2:任意角度旋转场景
如果需要支持任意角度的旋转同时触发重布局,需要自定义渲染组件,在布局阶段根据旋转角度计算自身的外接矩形尺寸,将旋转后的约束传递给子组件,实现重排:
class LayoutAwareRotatedBox extends SingleChildRenderObjectWidget { final double angle; const LayoutAwareRotatedBox({ super.key, required this.angle, super.child, }); @override RenderObject createRenderObject(BuildContext context) { return _RenderLayoutAwareRotatedBox(angle); } @override void updateRenderObject(BuildContext context, _RenderLayoutAwareRotatedBox renderObject) { renderObject.angle = angle; } } class _RenderLayoutAwareRotatedBox extends RenderProxyBox { double _angle; _RenderLayoutAwareRotatedBox(this._angle); double get angle => _angle; set angle(double value) { if (_angle == value) return; _angle = value; markNeedsLayout(); // 角度变化时触发重布局 markNeedsPaint(); } @override void performLayout() { if (child == null) { size = Size.zero; return; } // 先让子组件按原始约束布局 child!.layout(constraints, parentUsesSize: true); final childSize = child!.size; // 计算旋转后的外接矩形尺寸 final cosA = cos(angle.abs()); final sinA = sin(angle.abs()); final newWidth = childSize.width * cosA + childSize.height * sinA; final newHeight = childSize.width * sinA + childSize.height * cosA; // 自身尺寸按旋转后的外接矩形设置,触发父级重布局 size = constraints.constrain(Size(newWidth, newHeight)); } @override void paint(PaintingContext context, Offset offset) { if (child == null) return; // 旋转绘制,居中对齐 context.pushTransform( needsCompositing, offset, Matrix4.identity() ..translate(size.width / 2, size.height / 2) ..rotateZ(angle) ..translate(-child!.size.width / 2, -child!.size.height / 2), (context, offset) => child!.paint(context, offset), ); } }
使用时直接用TweenAnimationBuilder驱动angle参数即可,角度变化时会自动触发重布局:
@override Widget build(BuildContext context) { return TweenAnimationBuilder<double>( tween: Tween<double>(begin: 0, end: pi / 2), duration: const Duration(milliseconds: 500), builder: (context, angle, child) { return LayoutAwareRotatedBox( angle: angle, child: Stack( children: [ Image.network('https://i.imgur.com/Xl3LjXZ.jpeg'), Image.network('https://i.imgur.com/tF3g1JA.jpeg', width: 30, height: 30), ], ), ); }, ); }
以上两种方案都不会对子组件做缩放处理,完全保留子组件的原始尺寸,仅调整布局约束和绘制变换。
内容的提问来源于stack exchange,提问作者Andriy Trubchanin
相关产品推荐
相关产品推荐

