Search code examples
listviewflutterscrollroutestransition

How to make ListView preserve its scroll when transitioning to another route?


I want to accomplish slide to right transition in my flutter application. The problem is that route transition kinda creates new instance of page that I want to transit from, and so ListView scroll resets.

See a video

That's how I create a new route

/// @oldRoute needed cause this route transition utilizes `SlideStackRightRoute`
Route createSettingsRoute(Widget oldRoute) {
  return SlideStackRightRoute(exitPage: oldRoute, enterPage: SettingsRoute());
}

And finally slide transition class itself

import 'package:flutter/material.dart';

/// Creates cupertino-like route transition, where new route pushes old from right to left
class SlideStackRightRoute extends PageRouteBuilder {
  final Widget enterPage;
  final Widget exitPage;
  static var exBegin = Offset(0.0, 0.0);
  static var exEnd = Offset(-0.5, 0.0);
  static var entBegin = Offset(1.0, 0.0);
  static var entEnd = Offset.zero;
  static var curveIn = Curves.easeOutSine;
  static var curveOut = Curves.easeInSine;

  SlideStackRightRoute({@required this.exitPage, @required this.enterPage})
      : super(
          transitionDuration: Duration(milliseconds: 400),
          pageBuilder: (
            BuildContext context,
            Animation<double> animation,
            Animation<double> secondaryAnimation,
          ) =>
              enterPage,
          transitionsBuilder: (
            BuildContext context,
            Animation<double> animation,
            Animation<double> secondaryAnimation,
            Widget child,
          ) =>
              Stack(
            children: <Widget>[
              SlideTransition(
                position: Tween(begin: exBegin, end: exEnd)
                    .chain(CurveTween(curve: curveIn))
                    .chain(CurveTween(curve: curveOut))
                    .animate(animation),
                child: Container(
                    foregroundDecoration: BoxDecoration(
                      color: Colors.black.withOpacity(animation.value / 2),
                    ),
                    child: exitPage),
              ),
              SlideTransition(
                position: Tween(begin: entBegin, end: entEnd)
                    .chain(CurveTween(curve: curveIn))
                    .chain(CurveTween(curve: curveOut))
                    .animate(animation),
                child: enterPage,
              )
            ],
          ),
        );
}


Solution

  • the way i created a "slide-out" animation in the question wasn't correct. in terms of the framework it's called a secondaryAnimation

    to create your own secondary animation, you need to use a PageRouteBuilder transitionBuilder property

    example can the code below, which produces such animation, and there's no problem with a ListView

    enter image description here

    import 'package:flutter/material.dart';
    
    void main() => runApp(MyApp());
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(primaryColor: Colors.white),
          initialRoute: '/',
          onGenerateInitialRoutes: (initialRoute) => [createCustomTransition(HomeScreen())],
          onGenerateRoute: (settings) {
            if (settings.name == '1') {
              return createCustomTransition(SomeScreen());
            }
            return createCustomTransition(OtherScreen());
          },
          debugShowCheckedModeBanner: false,
        );
      }
    }
    
    /// Will create a custom route transition for you. 
    PageRouteBuilder createCustomTransition(Widget screen) {
      return PageRouteBuilder(
        transitionDuration: const Duration(milliseconds: 700),
        reverseTransitionDuration: const Duration(milliseconds: 700),
        pageBuilder: (context, animation, secondaryAnimation) => screen,
        transitionsBuilder: (context, animation, secondaryAnimation, child) {
          
          final slideAnimation = Tween(
            begin: const Offset(1.0, 0.0),
            end: Offset.zero,
          ).animate(CurvedAnimation(
            curve: Curves.easeOutCubic,
            reverseCurve: Curves.easeInCubic,
            parent: animation,
          ));
    
          final slideOutAnimation = Tween(
            begin: Offset.zero,
            end: const Offset(-0.3, 0.0),
          ).animate(CurvedAnimation(
            curve: Curves.easeOutCubic,
            reverseCurve: Curves.easeInCubic,
            parent: secondaryAnimation,
          ));
    
          return SlideTransition(
            position: slideAnimation,
            child: SlideTransition(
              position: slideOutAnimation,
              child: child,
            ),
          );
        },
      );
    }
    
    class HomeScreen extends StatelessWidget {
      HomeScreen({Key key}) : super(key: key);
      final List<int> list = List.generate(1000, (index) => index);
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: SafeArea(
            child: Column(
              children: [
                Expanded(
                  child: ListView.builder(
                    itemCount: list.length,
                    itemBuilder: (context, index) => ListTile(
                      title: Center(
                        child: Text(list[index].toString()),
                      )
                    ),
                  )
                ),
                ElevatedButton(
                  child: const Text('go to some screen'),
                  onPressed: () {
                    Navigator.of(context).pushNamed('1');
                  },
                ),
              ],
            ),
          ),
        );
      }
    }
    class SomeScreen extends StatelessWidget {
      const SomeScreen({Key key}) : super(key: key);
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          backgroundColor: Colors.red,
          appBar: AppBar(),
          body: Center(
            child: ElevatedButton(
              child: const Text('go to other screen'),
              onPressed: () {
                Navigator.of(context).pushNamed('');
              },
            ),
          ),
        );
      }
    }
    
    class OtherScreen extends StatelessWidget {
      const OtherScreen({Key key}) : super(key: key);
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          backgroundColor: Colors.blue,
          appBar: AppBar(),
        );
      }
    }
    
    

    according to docs PageRouteBuilder is

    A utility class for defining one-off page routes in terms of callbacks.

    it's perfrect for general use, but if you are building something more complex, i suggest taking a look at some framework page route animation impementations and different class relationships they have