如何在Flutter中使Scaffold的body和bottomSheet一起移动?

3
我正在尝试为我的Scaffold实现一种特定的行为,当显示BottomSheet时。我希望Scaffold的主体随着底部表单一起移动。也就是说,当BottomSheet出现时,Scaffold的主体应该随之上移。就像右侧的图像一样。我不确定我的方法是否正确。也许还有其他更好的选择来实现这种行为。

enter image description 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,
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    ),
  ),
);

1
你为什么不能直接使用ListView呢?如果所有内容都将成为单个可滚动的部分,那么你可以让ListView中的最后一项看起来像是一个可拖动的面板。 - Banjoe
问题是我不希望在滚动时看到“底部工作表”。如果我理解你的意思,那么这将是情况,不是吗?例如,我想要一个按钮,其功能是自动显示底部工作表,直到此时隐藏。 - Iván Yoed
你是不是指的是在iOS Chrome上底部导航栏消失时,需要向上滑动才能让它出现? - Gene
我更喜欢通过一个按钮或其他方式打开“底部工作表”,例如通过事件。 - Iván Yoed
3个回答

3
你可以使用一个 Stack 和两个 AnimatedPositioned 小部件来实现这一点:

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,
        ),
      ],
    );
  }
}

很棒的解决方案。我已经尝试过了,对我来说似乎非常可行。我将在我的应用程序中实现它,并给您反馈。 - Iván Yoed
好的,我明白了。我以为那就是你想要的。哈哈哈!我更新了答案并修复了它。 - Thierry
哈哈,不,你理解得很好。实际上,代码的第一个版本更好。问题在于,我希望能够在底部工作表打开或展开时滚动位于其后面的项目。但是,尽管我可以滚动,但您无法看到所有对象,例如最初位于应用程序顶部的第一个对象。如果需要,我可以制作一个gif。 - Iván Yoed
1
好的。看一下。我更改了管理ScrollController的方式以遵循您的要求。 我还重新组织了小部件,使BottomSheet功能位于单独的布局小部件中。我认为这将更易读和可维护。 告诉我你的想法! - Thierry
我还上传了一个新的结果动画GIF。 - Thierry
显示剩余4条评论

1

您可以使用带视差效果的sliding_up_panel

@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"),
      ),
    ),
  );
}

1
这对我来说似乎是一个非常好的解决方案,但我宁愿不使用包来实现某件事情。 - Iván Yoed
1
别担心,随你便~ 一个单一的结果可以有许多方法,只需选择最适合自己的即可! - Jim

1

您可以添加一个新的小部件到列中,而不是显示底部表单

reserve:true是导航到底部的关键参数

例如:

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


完整示例:


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),
      ),
    );
  }
}


这似乎是一个不错的解决方案,但是这样你就会失去bottomSheet出现时的动画效果。在这种情况下,使用布尔值,它只是突然出现了。是这样吗? - Iván Yoed
1
是的,但是使用AnimatedContainer可以轻松实现动画效果,例如您可以更改AnimatedContainer的高度,从0到400,它将在给定的持续时间和曲线下为您执行动画。@IvánYoed - cipli onat
另一个解决方法可以是打开底部表格,您可以将最新项的高度设置为与底部表格相同。 - cipli onat
你会失去动画效果,如果主内容高度超过屏幕高度,你还需要处理向下滚动到底部的问题。 - Thierry

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接