You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.26 22:06:02