/ app / lib / core / transitions / expanding_tile_route.dart
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  }