expanding_tile_route.dart
1 import 'package:flutter/material.dart'; 2 3 /// A custom page route that animates a tile expanding to fill the screen, 4 /// then fades in the destination page. 5 class ExpandingTileRoute<T> extends PageRouteBuilder<T> { 6 final Rect sourceRect; 7 final Color tileColor; 8 final Widget page; 9 10 ExpandingTileRoute({ 11 required this.sourceRect, 12 required this.tileColor, 13 required this.page, 14 }) : super( 15 pageBuilder: (context, animation, secondaryAnimation) => page, 16 transitionDuration: const Duration(milliseconds: 500), 17 reverseTransitionDuration: const Duration(milliseconds: 400), 18 transitionsBuilder: (context, animation, secondaryAnimation, child) { 19 return _ExpandingTileTransition( 20 animation: animation, 21 sourceRect: sourceRect, 22 tileColor: tileColor, 23 child: child, 24 ); 25 }, 26 ); 27 } 28 29 class _ExpandingTileTransition extends StatelessWidget { 30 final Animation<double> animation; 31 final Rect sourceRect; 32 final Color tileColor; 33 final Widget child; 34 35 const _ExpandingTileTransition({ 36 required this.animation, 37 required this.sourceRect, 38 required this.tileColor, 39 required this.child, 40 }); 41 42 @override 43 Widget build(BuildContext context) { 44 final screenSize = MediaQuery.of(context).size; 45 final screenRect = Rect.fromLTWH(0, 0, screenSize.width, screenSize.height); 46 47 // Check if we're going forward or backward 48 final isForward = animation.status == AnimationStatus.forward || 49 animation.status == AnimationStatus.completed; 50 51 if (isForward) { 52 // Forward: Tile expands, then content fades in 53 final expandAnimation = CurvedAnimation( 54 parent: animation, 55 curve: const Interval(0.0, 0.6, curve: Curves.easeOutCubic), 56 ); 57 58 final fadeAnimation = CurvedAnimation( 59 parent: animation, 60 curve: const Interval(0.4, 1.0, curve: Curves.easeOut), 61 ); 62 63 return Stack( 64 children: [ 65 // Expanding tile background 66 AnimatedBuilder( 67 animation: expandAnimation, 68 builder: (context, _) { 69 final t = expandAnimation.value; 70 71 // Interpolate from source rect to full screen 72 final currentRect = Rect.lerp(sourceRect, screenRect, t)!; 73 74 // Border radius shrinks as tile expands 75 final borderRadius = BorderRadius.circular( 76 (1 - t) * 8.0, 77 ); 78 79 return Positioned( 80 left: currentRect.left, 81 top: currentRect.top, 82 width: currentRect.width, 83 height: currentRect.height, 84 child: Container( 85 decoration: BoxDecoration( 86 color: tileColor, 87 borderRadius: borderRadius, 88 ), 89 ), 90 ); 91 }, 92 ), 93 94 // Chat screen content fading in 95 FadeTransition( 96 opacity: fadeAnimation, 97 child: child, 98 ), 99 ], 100 ); 101 } else { 102 // Reverse: Slide to the right 103 final slideAnimation = Tween<Offset>( 104 begin: Offset.zero, 105 end: const Offset(1.0, 0.0), 106 ).animate(CurvedAnimation( 107 parent: ReverseAnimation(animation), 108 curve: Curves.easeInOutCubic, 109 )); 110 111 return SlideTransition( 112 position: slideAnimation, 113 child: child, 114 ); 115 } 116 } 117 } 118 119 /// Helper to get the global rect of a widget using its GlobalKey 120 Rect? getWidgetRect(GlobalKey key) { 121 final renderBox = key.currentContext?.findRenderObject() as RenderBox?; 122 if (renderBox == null) return null; 123 124 final position = renderBox.localToGlobal(Offset.zero); 125 final size = renderBox.size; 126 127 return Rect.fromLTWH(position.dx, position.dy, size.width, size.height); 128 }