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

Flutter中心圆形展开式页面过渡效果实现求助

Flutter实现中心圆形展开的页面过渡效果

下面提供两种满足需求的实现方案,分别对应你提到的两种思路:


方案一:通过裁剪第一个页面实现圆形展开过渡

这种方式直接对第一个页面进行圆形裁剪,随着裁剪区域从中心向外扩大,逐步显示底层的第二个页面。如果需要用自定义Widget替代中心圆形区域,也可以在过渡层添加对应组件并配合动画调整显示状态。

实现代码

import 'package:flutter/material.dart';
import 'dart:math';

class CircleExpandTransition extends StatefulWidget {
  final Widget firstPage;
  final Widget secondPage;
  final AnimationController controller;

  const CircleExpandTransition({
    super.key,
    required this.firstPage,
    required this.secondPage,
    required this.controller,
  });

  @override
  State<CircleExpandTransition> createState() => _CircleExpandTransitionState();
}

class _CircleExpandTransitionState extends State<CircleExpandTransition> {
  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        // 底层是目标页面
        widget.secondPage,
        // 上层是被裁剪的初始页面
        AnimatedBuilder(
          animation: widget.controller,
          builder: (context, child) {
            return ClipPath(
              clipper: _CircleExpandClipper(
                progress: widget.controller.value,
                screenSize: MediaQuery.of(context).size,
              ),
              child: child,
            );
          },
          child: widget.firstPage,
        ),
        // 可选:添加中心自定义Widget,随过渡动画隐藏/缩放
        AnimatedBuilder(
          animation: widget.controller,
          builder: (context, child) {
            return Opacity(
              opacity: 1 - widget.controller.value,
              child: Transform.scale(
                scale: 1 + widget.controller.value * 1.5,
                child: child,
              ),
            );
          },
          child: const Center(
            child: Icon(Icons.navigate_next, size: 60, color: Colors.white),
          ),
        ),
      ],
    );
  }
}

class _CircleExpandClipper extends CustomClipper<Path> {
  final double progress;
  final Size screenSize;

  _CircleExpandClipper({required this.progress, required this.screenSize});

  @override
  Path getClip(Size size) {
    final center = Offset(screenSize.width / 2, screenSize.height / 2);
    final maxRadius = sqrt(screenSize.width * screenSize.width + screenSize.height * screenSize.height) / 2;
    final currentRadius = maxRadius * progress;

    // 绘制反向路径:先填充整个屏幕,再减去圆形区域
    final path = Path()
      ..addRect(Rect.fromLTWH(0, 0, screenSize.width, screenSize.height))
      ..addOval(Rect.fromCircle(center: center, radius: currentRadius))
      ..fillType = PathFillType.evenOdd;

    return path;
  }

  @override
  bool shouldReclip(covariant _CircleExpandClipper oldClipper) {
    return progress != oldClipper.progress;
  }
}

使用示例

class TransitionDemo extends StatefulWidget {
  const TransitionDemo({super.key});

  @override
  State<TransitionDemo> createState() => _TransitionDemoState();
}

class _TransitionDemoState extends State<TransitionDemo> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  bool _isSecondPageShown = false;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 800),
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  void _toggleTransition() {
    setState(() => _isSecondPageShown = !_isSecondPageShown);
    _isSecondPageShown ? _controller.forward() : _controller.reverse();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: CircleExpandTransition(
        firstPage: Scaffold(
          backgroundColor: Colors.deepPurple,
          body: Center(
            child: ElevatedButton(
              onPressed: _toggleTransition,
              child: const Text('展开到第二个页面', style: TextStyle(fontSize: 18)),
            ),
          ),
        ),
        secondPage: Scaffold(
          backgroundColor: Colors.teal,
          body: Center(
            child: ElevatedButton(
              onPressed: _toggleTransition,
              child: const Text('返回第一个页面', style: TextStyle(fontSize: 18)),
            ),
          ),
        ),
        controller: _controller,
      ),
    );
  }
}

方案二:带透明孔洞的遮罩动画

这种方式通过在第一个页面上方绘制带透明孔洞的遮罩,随着孔洞从中心向外扩大,逐步显示下方的第二个页面,适合需要保留第一个页面全部内容,仅通过遮罩控制显示区域的场景。

实现代码

import 'package:flutter/material.dart';
import 'dart:math';

class HoleExpandTransition extends StatefulWidget {
  final Widget backgroundPage;
  final Widget foregroundPage;
  final AnimationController controller;
  final Color maskColor;

  const HoleExpandTransition({
    super.key,
    required this.backgroundPage,
    required this.foregroundPage,
    required this.controller,
    required this.maskColor,
  });

  @override
  State<HoleExpandTransition> createState() => _HoleExpandTransitionState();
}

class _HoleExpandTransitionState extends State<HoleExpandTransition> {
  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        // 底层目标页面
        widget.backgroundPage,
        // 上层初始页面+遮罩
        Stack(
          children: [
            widget.foregroundPage,
            AnimatedBuilder(
              animation: widget.controller,
              builder: (context, _) {
                return CustomPaint(
                  size: MediaQuery.of(context).size,
                  painter: _HoleMaskPainter(
                    progress: widget.controller.value,
                    screenSize: MediaQuery.of(context).size,
                    maskColor: widget.maskColor,
                  ),
                );
              },
            ),
          ],
        ),
      ],
    );
  }
}

class _HoleMaskPainter extends CustomPainter {
  final double progress;
  final Size screenSize;
  final Color maskColor;

  _HoleMaskPainter({
    required this.progress,
    required this.screenSize,
    required this.maskColor,
  });

  @override
  void paint(Canvas canvas, Size size) {
    final maskPaint = Paint()..color = maskColor;
    final clearPaint = Paint()..blendMode = BlendMode.clear;

    // 绘制全屏遮罩
    canvas.drawRect(Rect.fromLTWH(0, 0, screenSize.width, screenSize.height), maskPaint);

    // 绘制透明孔洞
    final center = Offset(screenSize.width / 2, screenSize.height / 2);
    final maxRadius = sqrt(screenSize.width * screenSize.width + screenSize.height * screenSize.height) / 2;
    final currentRadius = maxRadius * progress;
    canvas.drawCircle(center, currentRadius, clearPaint);
  }

  @override
  bool shouldRepaint(covariant _HoleMaskPainter oldDelegate) {
    return progress != oldDelegate.progress || maskColor != oldDelegate.maskColor;
  }
}

使用示例

只需将CircleExpandTransition替换为HoleExpandTransition,并传入遮罩颜色(与第一个页面背景色一致):

HoleExpandTransition(
  backgroundPage: Scaffold(backgroundColor: Colors.orange, ...),
  foregroundPage: Scaffold(backgroundColor: Colors.blue, ...),
  controller: _controller,
  maskColor: Colors.blue,
)

内容的提问来源于stack exchange,提问作者Giacomo M

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 23:35:54