Implementing Custom Transitions Between Different Screens in Your App in Flutter

In Flutter, transitions play a vital role in enhancing user experience by providing smooth and visually appealing animations when navigating between different screens. While Flutter offers default transitions, implementing custom transitions allows you to create a unique and branded navigation experience. This article explores how to implement custom transitions between different screens in your Flutter app.

Why Custom Transitions?

Custom transitions provide several benefits:

  • Branding: Align transitions with your brand’s visual identity.
  • Improved UX: Create more engaging and intuitive navigation.
  • Differentiation: Stand out by offering unique and memorable interactions.

Methods for Implementing Custom Transitions

There are several ways to implement custom transitions in Flutter, including using PageRouteBuilder, Hero widgets, and animation controllers.

Method 1: Using PageRouteBuilder

PageRouteBuilder allows you to define custom transitions using animations and routing behaviors. This is one of the most flexible and commonly used approaches.

Step 1: Define Your Custom Transition

Create a function that returns a PageRouteBuilder with your custom animation.

import 'package:flutter/material.dart';

Route createRoute(Widget page) {
  return PageRouteBuilder(
    pageBuilder: (context, animation, secondaryAnimation) => page,
    transitionsBuilder: (context, animation, secondaryAnimation, child) {
      const begin = Offset(1.0, 0.0);
      const end = Offset.zero;
      const curve = Curves.ease;

      var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve));

      return SlideTransition(
        position: animation.drive(tween),
        child: child,
      );
    },
  );
}

In this example:

  • pageBuilder defines the widget to be displayed in the route.
  • transitionsBuilder defines the animation for the transition. This example uses a SlideTransition that slides the new page in from the right.
Step 2: Use the Custom Route

Use the custom route when navigating to a new screen.

Navigator.of(context).push(createRoute(const SecondScreen()));

Complete Example:

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Custom Transition Example',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const FirstScreen(),
    );
  }
}

class FirstScreen extends StatelessWidget {
  const FirstScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('First Screen'),
      ),
      body: Center(
        child: ElevatedButton(
          child: const Text('Go to Second Screen'),
          onPressed: () {
            Navigator.of(context).push(createRoute(const SecondScreen()));
          },
        ),
      ),
    );
  }
}

class SecondScreen extends StatelessWidget {
  const SecondScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Second Screen'),
      ),
      body: const Center(
        child: Text('This is the Second Screen'),
      ),
    );
  }
}

Route createRoute(Widget page) {
  return PageRouteBuilder(
    pageBuilder: (context, animation, secondaryAnimation) => page,
    transitionsBuilder: (context, animation, secondaryAnimation, child) {
      const begin = Offset(1.0, 0.0);
      const end = Offset.zero;
      const curve = Curves.ease;

      var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve));

      return SlideTransition(
        position: animation.drive(tween),
        child: child,
      );
    },
  );
}

Method 2: Using Hero Widgets

The Hero widget allows you to create “hero” animations between screens. This is especially useful when transitioning a specific widget from one screen to another.

Step 1: Define Hero Widgets in Both Screens

Wrap the widgets you want to animate with the Hero widget, giving them the same tag.

import 'package:flutter/material.dart';

class FirstScreen extends StatelessWidget {
  const FirstScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('First Screen'),
      ),
      body: Center(
        child: Hero(
          tag: 'hero-image',
          child: GestureDetector(
            onTap: () {
              Navigator.of(context).push(
                MaterialPageRoute(builder: (context) => const SecondScreen()),
              );
            },
            child: Image.network(
              'https://via.placeholder.com/150',
            ),
          ),
        ),
      ),
    );
  }
}

class SecondScreen extends StatelessWidget {
  const SecondScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Second Screen'),
      ),
      body: Center(
        child: Hero(
          tag: 'hero-image',
          child: Image.network(
            'https://via.placeholder.com/300',
            width: 300,
            height: 300,
          ),
        ),
      ),
    );
  }
}

In this example:

  • Both screens contain a Hero widget with the same tag (‘hero-image’).
  • When navigating from the first screen to the second, Flutter automatically animates the widget’s transition between the two screens.
Step 2: Navigate Between Screens

Navigate to the new screen using MaterialPageRoute.

Navigator.of(context).push(
  MaterialPageRoute(builder: (context) => const SecondScreen()),
);

Complete Example:

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Hero Transition Example',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const FirstScreen(),
    );
  }
}

class FirstScreen extends StatelessWidget {
  const FirstScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('First Screen'),
      ),
      body: Center(
        child: Hero(
          tag: 'hero-image',
          child: GestureDetector(
            onTap: () {
              Navigator.of(context).push(
                MaterialPageRoute(builder: (context) => const SecondScreen()),
              );
            },
            child: Image.network(
              'https://via.placeholder.com/150',
            ),
          ),
        ),
      ),
    );
  }
}

class SecondScreen extends StatelessWidget {
  const SecondScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Second Screen'),
      ),
      body: Center(
        child: Hero(
          tag: 'hero-image',
          child: Image.network(
            'https://via.placeholder.com/300',
            width: 300,
            height: 300,
          ),
        ),
      ),
    );
  }
}

Method 3: Using AnimationController

For more complex animations, use AnimationController to create custom animations that you control manually.

Step 1: Set Up the Animation Controller

Initialize the animation controller and define your animation.

import 'package:flutter/material.dart';

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

  @override
  _AnimatedScreenState createState() => _AnimatedScreenState();
}

class _AnimatedScreenState extends State
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation _offsetAnimation;

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

    _offsetAnimation = Tween(
      begin: const Offset(1.0, 0.0),
      end: Offset.zero,
    ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeIn));

    _controller.forward();
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Animated Screen'),
      ),
      body: SlideTransition(
        position: _offsetAnimation,
        child: const Center(
          child: Text('This screen is animated!'),
        ),
      ),
    );
  }
}

In this example:

  • An AnimationController is created to manage the animation’s timeline.
  • An Animation<Offset> is defined to animate the position of the screen.
  • The animation starts automatically in initState by calling _controller.forward().
Step 2: Implement the Animation

Use the AnimatedScreen in your navigation.

ElevatedButton(
  child: const Text('Go to Animated Screen'),
  onPressed: () {
    Navigator.of(context).push(
      MaterialPageRoute(builder: (context) => const AnimatedScreen()),
    );
  },
)

Complete Example:

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'AnimationController Example',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const FirstScreen(),
    );
  }
}

class FirstScreen extends StatelessWidget {
  const FirstScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('First Screen'),
      ),
      body: Center(
        child: ElevatedButton(
          child: const Text('Go to Animated Screen'),
          onPressed: () {
            Navigator.of(context).push(
              MaterialPageRoute(builder: (context) => const AnimatedScreen()),
            );
          },
        ),
      ),
    );
  }
}

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

  @override
  _AnimatedScreenState createState() => _AnimatedScreenState();
}

class _AnimatedScreenState extends State
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation _offsetAnimation;

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

    _offsetAnimation = Tween(
      begin: const Offset(1.0, 0.0),
      end: Offset.zero,
    ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeIn));

    _controller.forward();
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Animated Screen'),
      ),
      body: SlideTransition(
        position: _offsetAnimation,
        child: const Center(
          child: Text('This screen is animated!'),
        ),
      ),
    );
  }
}

Tips for Effective Custom Transitions

  • Keep it Smooth: Aim for 60 FPS to ensure transitions feel fluid.
  • Stay Consistent: Use consistent animations across your app to avoid jarring experiences.
  • Test on Different Devices: Ensure animations perform well on a variety of devices.
  • Consider Performance: Avoid complex calculations within animation loops to prevent performance bottlenecks.

Conclusion

Implementing custom transitions in Flutter is an excellent way to enhance the visual appeal and user experience of your app. By using PageRouteBuilder, Hero widgets, or AnimationController, you can create unique and engaging navigation animations that set your app apart. Always ensure animations are smooth, consistent, and optimized for performance across different devices.