flutterpage-transition

Create a page transition animation from a DraggableScrollableSheet


I have two pages, and I want to be able to transition between the two by dragging the first down into the second. The second page then has a DraggableScrollableSheet at the bottom that I want to be able to drag back up to turn back into the first page. I'm very new to this, so I've asked ChatGPT to help me out, and while it's given me a decent solution for the first bit, it can't animate the dragging on the sheet as a page transition. The code it gave me is below. Is this even possible? If yes, how could I go about it? Thanks!

import 'package:flutter/material.dart';

class DragTransitionView extends StatefulWidget {
  @override
  State<DragTransitionView> createState() => _DragTransitionViewState();
}

class _DragTransitionViewState extends State<DragTransitionView>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: Duration(milliseconds: 300),
    );
  }

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

  void _onFirstPageDragUpdate(DragUpdateDetails details) {
    // Dragging down increases the controller value
    if (details.primaryDelta! > 0) {
      _controller.value +=
          details.primaryDelta! / MediaQuery.of(context).size.height;
    }
  }

  void _onFirstPageDragEnd(DragEndDetails details) {
    if (_controller.value > 0.5) {
      _controller.forward(); // Complete transition to the second page
    } else {
      _controller.reverse(); // Return to the first page
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: [
          // Second Page
          FadeTransition(
            opacity: _controller,
            child: AnimatedBuilder(
              animation: _controller,
              builder: (context, child) {
                return Transform.translate(
                  offset: Offset(
                      0,
                      MediaQuery.of(context).size.height *
                          (1 - _controller.value)),
                  child: child,
                );
              },
              child: Container(
                color: Colors.blue,
                child: Stack(
                  children: [
                    Center(
                      child: Text(
                        'Second Page',
                        style: TextStyle(color: Colors.white, fontSize: 24),
                      ),
                    ),
                    // DraggableScrollableSheet
                    DraggableScrollableSheet(
                      initialChildSize: 0.3, // Initial height of the sheet
                      minChildSize: 0.1, // Minimum height
                      maxChildSize: 1.0, // Maximum height
                      builder: (BuildContext context,
                          ScrollController scrollController) {
                        return FadeTransition(
                          opacity: _controller,
                          child: Container(
                            color: Colors.white,
                            child: SingleChildScrollView(
                              controller: scrollController,
                              child: Center(
                                child: Text(
                                  'Drag Up from Here',
                                  style: TextStyle(
                                      color: Colors.black, fontSize: 18),
                                ),
                              ),
                            ),
                          ),
                        );
                      },
                    ),
                  ],
                ),
              ),
            ),
          ),
          // First Page
          GestureDetector(
            onVerticalDragUpdate: _onFirstPageDragUpdate,
            onVerticalDragEnd: _onFirstPageDragEnd,
            child: AnimatedBuilder(
              animation: _controller,
              builder: (context, child) {
                return Transform.translate(
                  offset: Offset(0,
                      MediaQuery.of(context).size.height * _controller.value),
                  child: Opacity(
                    opacity: 1 - _controller.value,
                    child: child,
                  ),
                );
              },
              child: Container(
                color: Colors.red,
                child: Center(
                  child: Text(
                    'First Page',
                    style: TextStyle(color: Colors.white, fontSize: 24),
                  ),
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

Solution

  • I ended up not using Navigator 1.0, as it was taking too long. But I did something with the DraggableScrollable that you wanted to implement. This might be a long code.

    import 'package:flutter/material.dart';
    import 'package:page_transition/expand_button.dart';
    
    class PageWidget extends StatefulWidget {
      const PageWidget({super.key});
    
      @override
      State<PageWidget> createState() => _PageWidgetState();
    }
    
    class _PageWidgetState extends State<PageWidget> {
      late final DraggableScrollableController dragController;
      @override
      void initState() {
        super.initState();
        dragController = DraggableScrollableController();
      }
    
      @override
      void dispose() {
        super.dispose();
        dragController.dispose();
      }
    
      void goToNextPage() {
        dragController.animateTo(0.9,
            duration: const Duration(milliseconds: 300), curve: Curves.easeInOut);
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: Stack(
            children: [
              //Page 2 Contents
              PageTwoContents(
                onTap: goToNextPage,
              ),
    
              //Lyrics
              Align(
                alignment: Alignment.bottomCenter,
                child: SizedBox(
                  // width: 400, // if you remove this it will fill the width
                  child: DraggableScrollableSheet(
                    controller: dragController,
                    minChildSize: 0.1,
                    maxChildSize: 0.9,
                    initialChildSize: 0.9,
                    builder:
                        (BuildContext context, ScrollController scrollController) {
                      //So some new layout can be built when constraints changes.
                      //lowkey just using it to setstate and switch shrink to expand.
                      return LayoutBuilder(
                        builder: (context, constraints) {
                          return Container(
                            padding: const EdgeInsets.symmetric(
                                vertical: 14.0, horizontal: 12.0),
                            decoration: const BoxDecoration(
                              color: Colors.blue,
                              borderRadius: BorderRadius.only(
                                topLeft: Radius.circular(6.0),
                                topRight: Radius.circular(6.0),
                              ),
                            ),
                            child: SingleChildScrollView(
                              controller: scrollController,
                              child: Column(
                                mainAxisAlignment: MainAxisAlignment.start,
                                children: [
                                  //Controls
                                  SizedBox(
                                    height: 100,
                                    child: Row(
                                      crossAxisAlignment: CrossAxisAlignment.start,
                                      mainAxisAlignment:
                                          MainAxisAlignment.spaceBetween,
                                      children: [
                                        const Padding(
                                          padding:
                                              EdgeInsets.symmetric(vertical: 8.0),
                                          child: Text(
                                            'This is Page 1',
                                            style: TextStyle(
                                              color: Colors.white,
                                              fontWeight: FontWeight.w600,
                                              fontSize: 14.0,
                                            ),
                                          ),
                                        ),
                                        IconButton(
                                          child:Text( !dragController.isAttached
                                              ? 'EXPAND'
                                              : dragController.size > 0.1
                                                  ? 'SHRINK'
                                                  : 'EXPAND',),
                                          onTap: () {
                                            if (dragController.size < 0.3) {
                                              dragController.animateTo(0.4,
                                                  duration: const Duration(
                                                      milliseconds: 300),
                                                  curve: Curves.easeInOut);
                                            } else {
                                              dragController.animateTo(0.0,
                                                  duration: const Duration(
                                                      milliseconds: 300),
                                                  curve: Curves.easeInOut);
                                            }
                                          },
                                        ),
                                      ],
                                    ),
                                  ),
    
                                  //Page 2 Contents
                                  const PageOneContents()
                                ],
                              ),
                            ),
                          );
                        },
                      );
                    },
                  ),
                ),
              ),
            ],
          ),
        );
      }
    }
    

    I placed the two widgets on one page, but using the draggable scrollable to transition between them, just like you wanted. I also placed PageOneContents and PageTwoContents in the exact locations that will give you the customization you seek. There is also an expand button on the two widgets to show you how you could control the transitions. Also Scrolling down page 1 will reveal Page 2. if you're curious about PageOneContent and PageTwoContent here they are.

    import 'package:flutter/material.dart';
    
    class PageTwoContents extends StatelessWidget {
      final VoidCallback? onTap;
      const PageTwoContents({super.key, this.onTap});
    
      @override
      Widget build(BuildContext context) {
        return Container(
          decoration: BoxDecoration(
            borderRadius: BorderRadius.circular(16.0),
            gradient: const LinearGradient(
              colors: [
                Color(0xEEE5EFFE),
                Color(0xEEF5EEF0),
              ],
              begin: Alignment.topCenter,
              end: Alignment.bottomCenter,
            ),
          ),
          child: Center(
              child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              const Text(
                'This is Page Two',
                style: TextStyle(
                  color: Colors.black,
                ),
              ),
              ElevatedButton(onPressed: onTap, child: const Text('Bring up Page 1'))
            ],
          )),
        );
      }
    }
    
    class PageOneContents extends StatelessWidget {
      const PageOneContents({super.key});
    
      @override
      Widget build(BuildContext context) {
        return const Align(
          alignment: Alignment.topCenter,
          child: SizedBox(
            child: Text(
              'The Sweet Chariot that takes you to valhalla is at the end of everything glorious\n\n'
              'May your focus ever remain as sharp as an Axe drenched in the sweat of blacksmiths from Kattegatt\n\n'
              'May your horse ride Bold and True\n\n'
              'May your eyes stay sharp\n\n'
              'May Odin welcome you with Joy for you have been Victorious\n\n',
              style: TextStyle(
                color: Colors.white,
                fontWeight: FontWeight.w600,
                fontSize: 14.0,
              ),
            ),
          ),
        );
      }
    }
    

    Here is a gif of how it looks

    Scrollable Sheet Transition