How to move Scaffold's body along with bottomSheet on Flutter?

Viewed 2262

I'm trying to achieve a particular behavior for my Scaffold when showing a BottomSheet. I want the Scaffold's body to move along with the bottom sheet. That is, when the Bottomheet comes out, the body of the Scaffold should go up with it. Like the image at the right. I'm not sure if my approach is the correct one. Maybe there are other better options to make this behavior possible.

enter image description here

The code with which I'm currently working is here:

 Scaffold(
  backgroundColor: Colors.purple[100],
  resizeToAvoidBottomInset: true,
  body: SingleChildScrollView(
    scrollDirection: Axis.vertical,
    child: Container(
      height: 900,
      child: Builder(
        builder: (context) => Container(
          child: GestureDetector(
            behavior: HitTestBehavior.translucent,
            onTap: () {
              FocusScope.of(context).requestFocus(_focusNode);
              if (bottomSheetIsOpen) {
                bottomSheetIsOpen = false;
                Navigator.of(context).pop();
              }
            },
            child: Container(
              width: double.infinity,
              height: double.infinity,
              child: Column(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  SizedBox(height: 50),
                  Container(
                    decoration: BoxDecoration(
                      color: Colors.white,
                      borderRadius: BorderRadius.circular(10),
                    ),
                    width: 300,
                    child: TextField(
                      cursorWidth: 3,
                      cursorColor: Colors.purple,
                      onTap: () {
                        bottomSheetIsOpen = true;
                        showBottomSheet(
                          clipBehavior: Clip.hardEdge,
                          context: context,
                          builder: (context) => Container(
                            child: Container(
                              height: 200,
                              color: Colors.red,
                            ),
                          ),
                        );
                      },
                      controller: _controller,
                      decoration: InputDecoration(
                        border: OutlineInputBorder(
                          borderRadius: BorderRadius.circular(10),
                        ),
                      ),
                      style: TextStyle(fontSize: 24),
                      showCursor: true,
                      readOnly: _readOnly,
                    ),
                  ),
                  Container(
                    height: 300,
                    width: 300,
                    color: Colors.yellow,
                  ),
                  Container(
                    height: 250,
                    width: 300,
                    color: Colors.orange,
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    ),
  ),
);
3 Answers

You could achieve this with a Stack and two AnimatedPositioned widget:

enter image description here

import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Bottomsheet Demo',
      debugShowCheckedModeBanner: false,
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends HookWidget {
  @override
  Widget build(BuildContext context) {
    final _isOpenBottomSheet = useState(false);
    return Scaffold(
      appBar: AppBar(title: Text('Bottomsheet Demo')),
      body: LayoutWithBottomSheet(
        children: List.generate(
          10,
          (index) => Container(
            height: 100,
            color: Colors.red.withGreen(index * 25),
            child: Center(
              child: Text(
                index.toString(),
                style: TextStyle(fontSize: 24.0),
              ),
            ),
          ),
        ).toList(),
        bottomSheetChild: Container(color: Colors.yellow),
        bottomSheetHeight: 400,
        animationSpeed: Duration(milliseconds: 300),
        animationCurve: Curves.easeInOutQuad,
        isOpenBottomSheet: _isOpenBottomSheet.value,
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          _isOpenBottomSheet.value = !_isOpenBottomSheet.value;
        },
        child: Icon(_isOpenBottomSheet.value
            ? Icons.arrow_downward
            : Icons.arrow_upward),
      ),
    );
  }
}

class LayoutWithBottomSheet extends HookWidget {
  final List<Widget> children;
  final Widget bottomSheetChild;
  final Duration animationSpeed;
  final Curve animationCurve;
  final double bottomSheetHeight;
  final bool isOpenBottomSheet;

  const LayoutWithBottomSheet({
    Key key,
    this.children,
    this.bottomSheetChild,
    this.animationSpeed,
    this.animationCurve,
    this.bottomSheetHeight,
    this.isOpenBottomSheet,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final _scrollController = useScrollController();
    final childrenBottom = useState<double>();
    final bottomSheetBottom = useState<double>();
    useEffect(() {
      if (isOpenBottomSheet) {
        childrenBottom.value = bottomSheetHeight;
        bottomSheetBottom.value = 0;
        if (_scrollController.hasClients) {
          Future.microtask(
            () => _scrollController.animateTo(
              _scrollController.offset + bottomSheetHeight,
              duration: animationSpeed,
              curve: animationCurve,
            ),
          );
        }
      } else {
        childrenBottom.value = 0;
        bottomSheetBottom.value = -bottomSheetHeight;
        if (_scrollController.hasClients) {
          _scrollController.animateTo(
            _scrollController.offset - bottomSheetHeight,
            duration: animationSpeed,
            curve: animationCurve,
          );
        }
      }
      return;
    }, [isOpenBottomSheet]);
    return Stack(
      children: [
        AnimatedPositioned(
          duration: animationSpeed,
          curve: animationCurve,
          left: 0,
          right: 0,
          top: 0,
          bottom: childrenBottom.value,
          child: ListView(
            controller: _scrollController,
            children: children,
          ),
        ),
        AnimatedPositioned(
          duration: animationSpeed,
          curve: animationCurve,
          left: 0,
          right: 0,
          bottom: bottomSheetBottom.value,
          height: bottomSheetHeight,
          child: bottomSheetChild,
        ),
      ],
    );
  }
}

Instead of showing a bottom sheet, you can add a new widget to a Column

reserve:true is the key parameter for navigating to bottom

like:

return Scaffold(
  body: SingleChildScrollView(
    reserve: true,
    child: Column(
      children: [
        YourWidget(),
        if (isOpenBottomSheet)
          YourBottomSheet()
      ],
    ),
  ),
);


the complete example:


import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

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

class _MyHomePageState extends State<MyHomePage> {
  bool isOpenBottomSheet = false;
  final _controller = ScrollController();

  void _incrementCounter() {
    setState(() {
      isOpenBottomSheet = !isOpenBottomSheet;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: SingleChildScrollView(
        controller: _controller,
        reverse: true,
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            // your widget
            Container(
                height: MediaQuery.of(context).size.height,
                color: Colors.black),
            // your bottom sheet
            if (isOpenBottomSheet) Container(height: 400, color: Colors.yellow),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

you can use sliding_up_panel with parallax effect:

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text("SlidingUpPanelExample"),
    ),
    body: SlidingUpPanel(
      parallaxEnabled: true,
      parallaxOffset: 0.4
      panel: Center(
        child: Text("This is the sliding Widget"),
      ),
      body: Center(
        child: Text("This is the Widget behind the sliding panel"),
      ),
    ),
  );
}
Related