【问题标题】:Standard Bottom Sheet in FlutterFlutter 中的标准底板
【发布时间】:2019-11-05 14:23:00
【问题描述】:

我很难在我的应用程序中实现“标准底部工作表”——我的意思是底部工作表,其中“标题”可见且可拖动(参考:https://material.io/design/components/sheets-bottom.html#standard-bottom-sheet)。甚至更多:我在任何地方都找不到它的任何例子:S。我希望的结果是通过将 DraggableScrollableSheet 实现为 bottomSheet:在 Scaffold 中(只有该小部件具有 initialChildSize ) 但接缝好像没有办法让标题“粘”,因为所有内容都是可滚动的:/。

我还发现了这个:https://flutterdoc.com/bottom-sheets-in-flutter-ec05c90453e7 - 像那里的接缝一样,关于“持久底片”的部分是我正在寻找的部分,但文章并不详细,所以我无法准确地弄清楚实现它的方式加上那里的 cmets 是 preaty 负数,所以我想这并不完全正确......

有人有解决办法吗?:S

【问题讨论】:

    标签: flutter bottom-sheet


    【解决方案1】:

    如果您正在寻找 Persistent Bottomsheet,请参考以下链接中的源代码

    Persistent Bottomsheet

    您可以参考 _showBottomSheet() 来满足您的要求,一些更改将满足您的要求

    【讨论】:

    • 您好,先生,看到并尝试过,但无法弄清楚“initialSize”和“minHeight”(向下拖动时不会完全隐藏) - 尝试添加“约束:BoxConstraints”到“容器”并设置 minHeight 但它不起作用:S
    【解决方案2】:

    您可以使用堆栈和动画来做到这一点:

    
    class HelloWorldPage extends StatefulWidget {
      @override
      _HelloWorldPageState createState() => _HelloWorldPageState();
    }
    
    class _HelloWorldPageState extends State<HelloWorldPage>
        with SingleTickerProviderStateMixin {
      final double minSize = 80;
      final double maxSize = 350;
    
      void initState() {
        _controller =
            AnimationController(vsync: this, duration: Duration(milliseconds: 500))
              ..addListener(() {
                setState(() {});
              });
    
        _animation =
            Tween<double>(begin: minSize, end: maxSize).animate(_controller);
    
        super.initState();
      }
    
      AnimationController _controller;
      Animation<double> _animation;
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: Stack(
            fit: StackFit.expand,
            children: <Widget>[
              Positioned(
                bottom: 0,
                height: _animation.value,
                child: GestureDetector(
                  onDoubleTap: () => _onEvent(),
                  onVerticalDragEnd: (event) => _onEvent(),
                  child: Container(
                    color: Colors.red,
                    width: MediaQuery.of(context).size.width,
                    height: minSize,
                  ),
                ),
              ),
            ],
          ),
        );
      }
    
      _onEvent() {
        if (_controller.isCompleted) {
          _controller.reverse(from: maxSize);
        } else {
          _controller.forward();
        }
      }
    
      @override
      void dispose() {
        _controller.dispose();
        super.dispose();
      }
    }
    
    

    【讨论】:

    • 嗨@Sergio - 请问:这是否意味着包含在颤振中的bottomSheet在这种情况下是没有意义的并且不能像我想要的那样进行调整?
    • 不,我认为您不能设置最小尺寸以保持底部工作表可见。可能你需要的是一个背景。看看这个背景的例子:youtube.com/watch?v=LcEyi1_1bAw 然后你可以根据需要调整它。希望能帮助到你!或查看其他示例,它更容易:medium.com/flutter/decomposing-widgets-backdrop-b5c664fb9cf4
    • 好的,完美,请输入信息 - 将检查这两个想法(使用堆栈和背景):)
    【解决方案3】:

    正如@Sergio 列举了一些不错的替代方案,它仍然需要更多的编码才能使其正常工作,我发现 Sliding_up_panel 所以对于其他寻找解决方案的人来说,你可以找到它here .

    不过,我觉得 Flutter 中内置的 bottomSheet 小部件并没有提供用于创建 material.io 中提到的“标准底页”的选项真的很奇怪:S

    【讨论】:

      【解决方案4】:

      可以使用DraggableScrollableSheet 来实现您在材料规格中看到的标准底页行为。

      这里我会详细解释一下。

      第 1 步:

      定义你的Scaffold

      import 'package:flutter/material.dart';
      
      void main() => runApp(MyApp());
      
      class MyApp extends StatelessWidget {
        @override
        Widget build(BuildContext context) {
          return MaterialApp(
            debugShowCheckedModeBanner: false,
            title: 'Draggable sheet demo',
            home: Scaffold(
      
                ///just for status bar color.
                appBar: PreferredSize(
                    preferredSize: Size.fromHeight(0),
                    child: AppBar(
                      primary: true,
                      elevation: 0,
                    )),
                body: Stack(
                  children: <Widget>[
                    Positioned(
                      left: 0.0,
                      top: 0.0,
                      right: 0.0,
                      child: PreferredSize(
                          preferredSize: Size.fromHeight(56.0),
                          child: AppBar(
                            title: Text("Standard bottom sheet demo"),
                            elevation: 2.0,
                          )),
                    ),
                    DraggableSearchableListView(),
                  ],
                )),
          );
        }
      }
      

      第 2 步:

      定义DraggableSearchableListView

       class DraggableSearchableListView extends StatefulWidget {
        const DraggableSearchableListView({
          Key key,
        }) : super(key: key);
      
        @override
        _DraggableSearchableListViewState createState() =>
            _DraggableSearchableListViewState();
      }
      
      class _DraggableSearchableListViewState
          extends State<DraggableSearchableListView> {
        final TextEditingController searchTextController = TextEditingController();
        final ValueNotifier<bool> searchTextCloseButtonVisibility =
            ValueNotifier<bool>(false);
        final ValueNotifier<bool> searchFieldVisibility = ValueNotifier<bool>(false);
        @override
        void dispose() {
          searchTextController.dispose();
          searchTextCloseButtonVisibility.dispose();
          searchFieldVisibility.dispose();
          super.dispose();
        }
      
        @override
        Widget build(BuildContext context) {
          return NotificationListener<DraggableScrollableNotification>(
            onNotification: (notification) {
              if (notification.extent == 1.0) {
                searchFieldVisibility.value = true;
              } else {
                searchFieldVisibility.value = false;
              }
              return true;
            },
            child: DraggableScrollableActuator(
              child: Stack(
                children: <Widget>[
                  DraggableScrollableSheet(
                    initialChildSize: 0.30,
                    minChildSize: 0.15,
                    maxChildSize: 1.0,
                    builder:
                        (BuildContext context, ScrollController scrollController) {
                      return Container(
                        decoration: BoxDecoration(
                          color: Colors.white,
                          borderRadius: BorderRadius.only(
                            topLeft: Radius.circular(16.0),
                            topRight: Radius.circular(16.0),
                          ),
                          boxShadow: [
                            BoxShadow(
                                color: Colors.grey,
                                offset: Offset(1.0, -2.0),
                                blurRadius: 4.0,
                                spreadRadius: 2.0)
                          ],
                        ),
                        child: ListView.builder(
                          controller: scrollController,
      
                          ///we have 25 rows plus one header row.  
                          itemCount: 25 + 1,
                          itemBuilder: (BuildContext context, int index) {
                            if (index == 0) {
                              return Container(
                                child: Column(
                                  children: <Widget>[
                                    Align(
                                      alignment: Alignment.centerLeft,
                                      child: Padding(
                                        padding: EdgeInsets.only(
                                          top: 16.0,
                                          left: 24.0,
                                          right: 24.0,
                                        ),
                                        child: Text(
                                          "Favorites",
                                          style:
                                              Theme.of(context).textTheme.headline6,
                                        ),
                                      ),
                                    ),
                                    SizedBox(
                                      height: 8.0,
                                    ),
                                    Divider(color: Colors.grey),
                                  ],
                                ),
                              );
                            }
                            return Padding(
                                padding: EdgeInsets.symmetric(horizontal: 16.0),
                                child: ListTile(title: Text('Item $index')));
                          },
                        ),
                      );
                    },
                  ),
                  Positioned(
                    left: 0.0,
                    top: 0.0,
                    right: 0.0,
                    child: ValueListenableBuilder<bool>(
                        valueListenable: searchFieldVisibility,
                        builder: (context, value, child) {
                          return value
                              ? PreferredSize(
                                  preferredSize: Size.fromHeight(56.0),
                                  child: Container(
                                    decoration: BoxDecoration(
                                      border: Border(
                                        bottom: BorderSide(
                                            width: 1.0,
                                            color: Theme.of(context).dividerColor),
                                      ),
                                      color: Theme.of(context).colorScheme.surface,
                                    ),
                                    child: SearchBar(
                                      closeButtonVisibility:
                                          searchTextCloseButtonVisibility,
                                      textEditingController: searchTextController,
                                      onClose: () {
                                        searchFieldVisibility.value = false;
                                        DraggableScrollableActuator.reset(context);
                                      },
                                      onSearchSubmit: (String value) {
                                        ///submit search query to your business logic component
                                      },
                                    ),
                                  ),
                                )
                              : Container();
                        }),
                  ),
                ],
              ),
            ),
          );
        }
      }
      

      第三步:

      定义自定义的粘性搜索栏

       class SearchBar extends StatelessWidget {
        final TextEditingController textEditingController;
        final ValueNotifier<bool> closeButtonVisibility;
        final ValueChanged<String> onSearchSubmit;
        final VoidCallback onClose;
      
        const SearchBar({
          Key key,
          @required this.textEditingController,
          @required this.closeButtonVisibility,
          @required this.onSearchSubmit,
          @required this.onClose,
        }) : super(key: key);
      
        @override
        Widget build(BuildContext context) {
          final ThemeData theme = Theme.of(context);
          return Container(
            child: Padding(
              padding: EdgeInsets.symmetric(horizontal: 0),
              child: Row(
                children: <Widget>[
                  SizedBox(
                    height: 56.0,
                    width: 56.0,
                    child: Material(
                      type: MaterialType.transparency,
                      child: InkWell(
                        child: Icon(
                          Icons.arrow_back,
                          color: theme.textTheme.caption.color,
                        ),
                        onTap: () {
                          FocusScope.of(context).unfocus();
                          textEditingController.clear();
                          closeButtonVisibility.value = false;
                          onClose();
                        },
                      ),
                    ),
                  ),
                  SizedBox(
                    width: 16.0,
                  ),
                  Expanded(
                    child: TextFormField(
                      onChanged: (value) {
                        if (value != null && value.length > 0) {
                          closeButtonVisibility.value = true;
                        } else {
                          closeButtonVisibility.value = false;
                        }
                      },
                      onFieldSubmitted: (value) {
                        FocusScope.of(context).unfocus();
                        onSearchSubmit(value);
                      },
                      keyboardType: TextInputType.text,
                      textInputAction: TextInputAction.search,
                      textCapitalization: TextCapitalization.none,
                      textAlignVertical: TextAlignVertical.center,
                      textAlign: TextAlign.left,
                      maxLines: 1,
                      controller: textEditingController,
                      decoration: InputDecoration(
                        isDense: true,
                        border: InputBorder.none,
                        hintText: "Search here",
                      ),
                    ),
                  ),
                  ValueListenableBuilder<bool>(
                      valueListenable: closeButtonVisibility,
                      builder: (context, value, child) {
                        return value
                            ? SizedBox(
                                width: 56.0,
                                height: 56.0,
                                child: Material(
                                  type: MaterialType.transparency,
                                  child: InkWell(
                                    child: Icon(
                                      Icons.close,
                                      color: theme.textTheme.caption.color,
                                    ),
                                    onTap: () {
                                      closeButtonVisibility.value = false;
                                      textEditingController.clear();
                                    },
                                  ),
                                ),
                              )
                            : Container();
                      })
                ],
              ),
            ),
          );
        }
      }
      

      查看最终输出的截图。

      状态 1:

      底部的表格显示了它的初始大小。

      状态 2:

      用户向上拖动底部工作表。

      状态 3:

      底部工作表到达屏幕的顶部边缘,并显示一个粘性自定义 SearchBar 界面。


      就是这样。

      观看现场演示here

      【讨论】:

      • 太棒了! - 将在下周测试它(坐在有角度的项目 atm 上)并将你的答案标记为正确的答案,ty vm!
      • @Darish woooow 真棒
      【解决方案5】:

      可以通过showModalBottomSheet 轻松实现。代码:

      void _presentBottomSheet(BuildContext context) {
        showModalBottomSheet(
          context: context,
          builder: (context) => Wrap(
            children: <Widget>[
              SizedBox(height: 8),
              _buildBottomSheetRow(context, Icons.share, 'Share'),
              _buildBottomSheetRow(context, Icons.link, 'Get link'),
              _buildBottomSheetRow(context, Icons.edit, 'Edit Name'),
              _buildBottomSheetRow(context, Icons.delete, 'Delete collection'),
            ],
          ),
        );
      }
      
      Widget _buildBottomSheetRow(
        BuildContext context,
        IconData icon,
        String text,
      ) =>
          InkWell(
            onTap: () {},
            child: Row(
              children: <Widget>[
                Padding(
                  padding: const EdgeInsets.all(16),
                  child: Icon(
                    icon,
                    color: Colors.grey[700],
                  ),
                ),
                SizedBox(width: 8),
                Text(text),
              ],
            ),
          );
      

      【讨论】:

        猜你喜欢
        • 2019-02-10
        • 1970-01-01
        • 2021-06-27
        • 1970-01-01
        • 2021-04-30
        • 2021-05-20
        • 2014-09-13
        • 2020-08-02
        • 2011-05-03
        相关资源
        最近更新 更多