Search code examples
flutterdartanimationnavigator

Flutter enable swipe back on Custom Transition FadeAnimation


I have a CustomPageRoute for a fade animation when pushing to another screen. That is working as expected.

However I would love to have a swipe back animation and can not make it work. I tried couple of different things, the most promising was this answer but that ist still sliding in/out the page.

For animation I only want the a fadeAnimation, both for pushing and popping.

This is my code for the FadeTransition:

class FadePageTransition extends Page {
  final Widget page;

  const FadePageTransition({
    required this.page,
    LocalKey? key,
    String? restorationId,
  }) : super(
          key: key,
          restorationId: restorationId,
        );

  @override
  Route createRoute(BuildContext context) {
    return FadeRoute(
      child: page,
      routeSettings: this,
    );
  }
}

class FadeRoute<T> extends PageRoute<T> {
  final Widget child;
  final RouteSettings routeSettings;

  FadeRoute({
    required this.child,
    required this.routeSettings,
  });

  @override
  RouteSettings get settings => routeSettings;

  @override
  Color? get barrierColor => Palette.black;

  @override
  String? get barrierLabel => null;

  @override
  Widget buildPage(
    BuildContext context,
    Animation<double> animation,
    Animation<double> secondaryAnimation,
  ) {
    return FadeTransition(
      opacity: animation,
      child: child,
    );
  }

  @override
  bool get maintainState => true;

  @override
  Duration get transitionDuration => const Duration(
        milliseconds: transitionDurationInMS,
      );
}

Let me know if you need any more info.

In iOS Swift it is possible, with this code:

// handle swqipe down gesture
@objc private func handlePan(gestureRecognizer:UIPanGestureRecognizer) {
    // calculate the progress based on how far the user moved
    let translation = panGR.translation(in: nil)
    let progress = translation.y / 2 / view.bounds.height
    
    switch panGR.state {
    case .began:
        // begin the transition as normal
        self.dismissView()
        break
    case .changed:
        
        Hero.shared.update(progress)
        
    default:
        // finish or cancel the transition based on the progress and user's touch velocity
        if progress + panGR.velocity(in: nil).y / view.bounds.height > 0.3 {
            self.dismissView()
            Hero.shared.finish()
        } else {
            Hero.shared.cancel()
        }
    }
}

Solution

  • I have extended CupertinoPageRoute and used buildTransitions method to get _CupertinoBackGestureDetector object which is responsible for detecting the back swipe.

    import 'package:flutter/cupertino.dart';
    import 'package:flutter/material.dart';
    
    void main() {
      runApp(const MyApp());
    }
    
    class MyApp extends StatelessWidget {
      const MyApp({super.key});
    
      @override
      Widget build(BuildContext context) {
        return Semantics(
          child: MaterialApp(
            title: 'Flutter Demo',
            theme: ThemeData(
              useMaterial3: false,
            ),
            onGenerateRoute: (settings) {
              if (settings.name == "/") {
                return FadePageRoute(builder: (context) => const HomePage());
              } else if (settings.name == "/abc") {
                return FadePageRoute(builder: (context) => const SecondPage());
              }
              return null;
            },
          ),
        );
      }
    }
    
    class FadePageRoute<T> extends CupertinoPageRoute<T> {
      FadePageRoute({required super.builder});
    
      @override
      Widget buildTransitions(BuildContext context, Animation<double> animation,
          Animation<double> secondaryAnimation, Widget child) {
        final widget =
            super.buildTransitions(context, animation, secondaryAnimation, child);
        if (widget is CupertinoPageTransition) {
          return FadeTransition(
            opacity: animation,
            child: widget.child,
          );
        } else {
          return widget;
        }
      }
    }
    
    class HomePage extends StatelessWidget {
      const HomePage({super.key});
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(),
          body: Center(
            child: TextButton(
              child: const Text('Next Page',
                  style: TextStyle(
                    fontSize: 48,
                  )),
              onPressed: () {
                Navigator.of(context).pushNamed('/abc');
              },
            ),
          ),
        );
      }
    }
    
    class SecondPage extends StatelessWidget {
      const SecondPage({super.key});
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(),
          body: const Center(
            child: Text(
              '2nd Page',
              style: TextStyle(
                fontSize: 48,
              ),
            ),
          ),
        );
      }
    }
    
    

    Output: https://youtube.com/shorts/dQpfyEH9frE

    Please let me know if this is something which you wanted.