Search code examples
flutterdartflutter-animationpage-transition

Page transition with circle in the center that expands revealing the second view


I am trying to do a Page transition using a circle in the center of the first view that expands revealing second view.

In alternative, it's the same if I use a widget with a transparent hole in the center (and we can see the bottom widget through the hole), and with an animation that expands the hole revealing the widget in the bottom.

The only answer I found similar to what I'm trying to do is this:

How to create widget with transparent hole in flutter

but I am not able to use a widget instead of the circle ping the center of the screen.


Solution

  • use HoleMaterialPageRoute instead of MaterialPageRoute - note that you can completely remove transitionDuration if the default duration is ok with you, also you can remove late final curve = CurvedAnimation( and use animation.value instead of curve.value but in my opinion non linear radius growth looks better :-)

    class HoleMaterialPageRoute extends MaterialPageRoute {
      HoleMaterialPageRoute({required super.builder});
    
      // the default transitionDuration is Duration(milliseconds: 300) so you can make
      // it longer to see the better effect:
      @override
      Duration get transitionDuration => const Duration(milliseconds: 800);
    
      @override
      Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
        return ClipPath(
          clipper: _HoleClipper(animation),
          child: child,
        );
      }
    }
    
    class _HoleClipper extends CustomClipper<Path> {
      _HoleClipper(this.animation);
    
      final Animation<double> animation;
      late final curve = CurvedAnimation(
        parent: animation,
        curve: Curves.easeOut,
        reverseCurve: Curves.easeIn,
      );
    
      @override
      Path getClip(Size size) {
        final center = size.center(Offset.zero);
        final radius = lerpDouble(0, center.distance, curve.value)!;
        // print('size: $size, radius: $radius');
        final oval = Rect.fromCircle(center: center, radius: radius);
        return Path()
          ..addOval(oval);
      }
    
      @override
      bool shouldReclip(covariant CustomClipper<Path> oldClipper) => true;
    }
    

    EDIT

    in some cases a better solution is to use ThemeData.pageTransitionsTheme so that you can use Navigator.pushNamed or Navigator.push with "normal" MaterialPageRoute (you dont have to change your Navigator related code)

    to do so you have to create a custom PageTransitionsBuilder:

    class HolePageTransitionsBuilder extends PageTransitionsBuilder {
      @override
      Widget buildTransitions<T>(PageRoute<T> route, BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
        return ClipPath(
          clipper: _HoleClipper(animation),
          child: child,
        );
      }
    }
    

    and use it like that (this is a case for linux platform where i tested my code):

    MaterialApp(
      theme: ThemeData.light(useMaterial3: false).copyWith(
        pageTransitionsTheme: PageTransitionsTheme(
          builders: {
            TargetPlatform.linux: HolePageTransitionsBuilder(),
          }
        )
      ),
      ...
    )