【问题标题】:How to use a Positioned widget in an AppBar如何在 AppBar 中使用定位小部件
【发布时间】:2018-07-09 03:46:24
【问题描述】:

从我读过的内容来看,这应该是可能的,但我无法让它发挥作用。我在appBarbottom 中有一个Stack,在Stack 中有一个Positioned 列表。一切似乎都按预期定位,但appBar 正在裁剪列表,因此如果appBarbody 的内容,列表不会显示在顶部。

我是 Flutter 的新手,但在 HTML 的世界中,我有一个绝对定位的列表,并且 appBar 的 z-index 将被固定,高于 body 以实现分层效果。

我尝试了很多变体,但似乎 appBar 想要裁剪它的子级。任何帮助将不胜感激。

这是我试图模仿的图片:

这是一段代码:

return new Scaffold(
  appBar: new AppBar(
    title: new Row(
      children: <Widget>[
        new Padding(
          padding: new EdgeInsets.only(
            right: 10.0,
          ),
          child: new Icon(Icons.shopping_basket),
        ),
        new Text(appTitle)
      ],
    ),
    bottom: new PreferredSize(
      preferredSize: const Size.fromHeight(30.0),
      child: new Padding(
        padding: new EdgeInsets.only(
          bottom: 10.0,
          left: 10.0,
          right: 10.0,
        ),
        child: new AutoCompleteInput(
          key: new ObjectKey('$completionList'),
          completionList: completionList,
          hintText: 'Add Item',
          onSubmit: _addListItem,
        ),
      ),
    ),
  ),

更新 #1

Widget build(BuildContext ctx) {
  final OverlayEntry _entry = new OverlayEntry(
    builder: (BuildContext context) => const Text('hi')
  );
  Overlay.of(ctx, debugRequiredFor: widget).insert(_entry);
  return new Row(

【问题讨论】:

  • 你能包含一些代码和想要的视觉效果吗?
  • @RémiRousselet - 添加和添加

标签: dart flutter flutter-positioned


【解决方案1】:

您将无法使用Positioned 小部件将某些内容绝对定位在剪辑之外。 (AppBar 要求此剪辑遵循材质规范,因此它可能不会更改)。

如果您需要在构建它的小部件边界“之外”放置一些东西,那么您需要一个Overlay。覆盖层本身是由MaterialApp 中的导航器创建的,因此您可以将新元素推入其中。使用Overlay 的其他一些小部件是工具提示和弹出菜单,因此如果您愿意,可以查看它们的实现以获得更多灵感。

final OverlayEntry entry = new OverlayEntry(builder: (BuildContext context) => /* ... */)
Overlay.of(context, debugRequiredFor: widget).insert(_entry);

【讨论】:

  • 啊,我想了一会儿AutoCompleteInput 是一个官方的 Flutter 小部件(应该是 btw)。所以是的,很可能是问题所在。
  • 嗯,规范暗示这可能对我有用。我还没有找到任何描述如何定位(例如将其放在输入下方)或调整叠加层大小的文档。除非覆盖占据整个屏幕,否则我只使用具有设定宽度和高度的位置。理想情况下,它会从父级继承它的宽度。
  • 正确,覆盖占据了整个屏幕,然后您可以相对于您的小部件定位它。这就是弹出菜单 sn-p 下面正在做的事情
  • 我并不是说规范说你不能弹出东西,只是应用栏需要素材上的那个剪辑来遵循某些行为,所以能够禁用是不合理的它
  • @JonahWilliams 我不知道在哪里使用您的示例。我有一个build 和一个包含TextFieldRowentry 是否位于 build 的返回上方?另外,在您的示例中,我假设您的意思是OverlayEntry _entryinsert(entry)。我环顾了互联网,找不到任何关于如何实际实现覆盖的好例子。关于如何覆盖小部件有很多问题,但我没有找到任何使用Overlay,只有Stack
【解决方案2】:

我从未对此进行过测试,但 AppBar 有一个 flexibleSpace 属性,该属性将小部件作为参数。此小部件放置在 AppBar 顶部(标题所在的位置)和 AppBar 底部之间的空间中。如果您将小部件放置在此空间中,而不是 AppBar 的底部(应仅用于 TabBar 等小部件),您的应用程序可能会正常工作。

另一种可能的解决方案是将列表元素放在DropdownButton 中,而不是放在堆栈中。

您可以在AppBarhere找到更多信息。

编辑:您还可以考虑使用 Scaffold 主体在使用搜索时显示建议。

此外,您可能会发现 PopupMenuButton 的源代码对解决您的问题很有用(因为它的工作方式与您的建议框类似)。这是一个sn-p:

  void showButtonMenu() {
    final RenderBox button = context.findRenderObject();
    final RenderBox overlay = Overlay.of(context).context.findRenderObject();
    final RelativeRect position = new RelativeRect.fromRect(
      new Rect.fromPoints(
        button.localToGlobal(Offset.zero, ancestor: overlay),
        button.localToGlobal(button.size.bottomRight(Offset.zero), ancestor: overlay),
      ),
      Offset.zero & overlay.size,
    );
    showMenu<T>(
      context: context,
      elevation: widget.elevation,
      items: widget.itemBuilder(context),
      initialValue: widget.initialValue,
      position: position,
    )
    .then<void>((T newValue) {
      if (!mounted)
        return null;
      if (newValue == null) {
        if (widget.onCanceled != null)
          widget.onCanceled();
        return null;
      }
      if (widget.onSelected != null)
        widget.onSelected(newValue);
    });
  }

【讨论】:

  • 我已经尝试过DropdownButton 方法,但我需要一个输入,以便在用户键入时显示列表。据说有一种方法可以以编程方式显示列表,但我无法让它工作。等我下班回家后,我会试试flexibleSpace
  • PopupMenuButton 的例子看起来很有趣,如果它的父级在 AppBar 中,它可以覆盖 AppBar 和 body 吗?
  • @theOneWhoKnocks 是的,如果它嵌套在 AppBar 中,它会覆盖它和主体。它的工作方式与 Android 应用中的 3 点菜单相同。
  • 我认为它定义它的部分应该覆盖它的父级在 sn-p 的第 6 行和第 7 行,但我不确定。
【解决方案3】:

创建了一个示例文件来演示我的想法(至少与此问题相关)。希望它能让其他人免于不必要的头痛。

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

String appTitle = 'Overlay Example';

class _CustomDelegate extends SingleChildLayoutDelegate {
  final Offset target;
  final double verticalOffset;

  _CustomDelegate({
    @required this.target,
    @required this.verticalOffset,
  }) : assert(target != null),
       assert(verticalOffset != null);

  @override
  BoxConstraints getConstraintsForChild(BoxConstraints constraints) => constraints.loosen();

  @override
  Offset getPositionForChild(Size size, Size childSize) {
    return positionDependentBox(
      size: size,
      childSize: childSize,
      target: target,
      verticalOffset: verticalOffset,
      preferBelow: true,
    );
  }

  @override
  bool shouldRelayout(_CustomDelegate oldDelegate) {
    return
      target != oldDelegate.target
      || verticalOffset != oldDelegate.verticalOffset;
  }
}

class _CustomOverlay extends StatelessWidget {
  final Widget child;
  final Offset target;

  const _CustomOverlay({
    Key key,
    this.child,
    this.target,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    double borderWidth = 2.0;
    Color borderColor = Theme.of(context).accentColor;

    return new Positioned.fill(
      child: new IgnorePointer(
        ignoring: false,
        child: new CustomSingleChildLayout(
          delegate: new _CustomDelegate(
            target: target,
            verticalOffset: -5.0,
          ),
          child: new Padding(
            padding: const EdgeInsets.symmetric(horizontal: 10.0),
            child: new ConstrainedBox(
              constraints: new BoxConstraints(
                maxHeight: 100.0,
              ),
              child: new Container(
                decoration: new BoxDecoration(
                  color: Colors.white,
                  border: new Border(
                    right: new BorderSide(color: borderColor, width: borderWidth),
                    bottom: new BorderSide(color: borderColor, width: borderWidth),
                    left: new BorderSide(color: borderColor, width: borderWidth),
                  ),
                ),
                child: child,
              ),
            ),
          ),
        ),
      ),
    );
  }
}

class _CustomInputState extends State<_CustomInput> {
  TextEditingController _inputController = new TextEditingController();
  FocusNode _focus = new FocusNode();
  List<String> _listItems;
  OverlayState _overlay;
  OverlayEntry _entry;
  bool _entryIsVisible = false;
  StreamSubscription _sub;

  void _toggleEntry(show) {
    if(_overlay.mounted && _entry != null){
      if(show){
        _overlay.insert(_entry);
        _entryIsVisible = true;
      }
      else{
        _entry.remove();
        _entryIsVisible = false;
      }
    }
    else {
      _entryIsVisible = false;
    }
  }

  void _handleFocus(){
    if(_focus.hasFocus){
      _inputController.addListener(_handleInput);
      print('Added input handler');
      _handleInput();
    }
    else{
      _inputController.removeListener(_handleInput);
      print('Removed input handler');
    }
  }

  void _handleInput() {
    String newVal = _inputController.text;

    if(widget.parentStream != null && _sub == null){
      _sub = widget.parentStream.listen(_handleStream);
      print('Added stream listener');
    }

    if(_overlay == null){
      final RenderBox bounds = context.findRenderObject();
      final Offset target = bounds.localToGlobal(bounds.size.bottomCenter(Offset.zero));

      _entry = new OverlayEntry(builder: (BuildContext context){
        return new _CustomOverlay(
          target: target,
          child: new Material(
            child: new ListView.builder(
              padding: const EdgeInsets.all(0.0),
              itemBuilder: (BuildContext context, int ndx) {
                String label = _listItems[ndx];
                return new ListTile(
                  title: new Text(label),
                  onTap: () {
                    print('Chose: $label');
                    _handleSubmit(label);
                  },
                );
              },
              itemCount: _listItems.length,
            ),
          ),
        );
      });
      _overlay = Overlay.of(context, debugRequiredFor: widget);
    }

    setState(() {
      // This can be used if the listItems get updated, which won't happen in
      // this example, but I figured it was useful info.
      if(!_entryIsVisible && _listItems.length > 0){
        _toggleEntry(true);
      }else if(_entryIsVisible && _listItems.length == 0){
        _toggleEntry(false);
      }else{
        _entry.markNeedsBuild();
      }
    });
  }

  void _exitInput(){
    if(_sub != null){
      _sub.cancel();
      _sub = null;
      print('Removed stream listener');
    }
    // Blur the input
    FocusScope.of(context).requestFocus(new FocusNode());
    // hide the list
    _toggleEntry(false);

  }

  void _handleSubmit(newVal) {
    // Set to selected value
    _inputController.text = newVal;
    _exitInput();
  }

  void _handleStream(ev) {
    print('Input Stream : $ev');
    switch(ev){
      case 'TAP_UP':
        _exitInput();
        break;
    }
  }

  @override
  void initState() {
    super.initState();
    _focus.addListener(_handleFocus);
    _listItems = widget.listItems;
  }

  @override
  void dispose() {
    _inputController.removeListener(_handleInput);
    _inputController.dispose();

    if(mounted){
      if(_sub != null) _sub.cancel();
      if(_entryIsVisible){
        _entry.remove();
        _entryIsVisible = false;
      }
      if(_overlay != null && _overlay.mounted) _overlay.dispose();
    }

    super.dispose();
  }

  @override
  Widget build(BuildContext ctx) {
    return new Row(
      children: <Widget>[
        new Expanded(
          child: new TextField(
            autocorrect: true,
            focusNode: _focus,
            controller: _inputController,
            decoration: new InputDecoration(
              border: new OutlineInputBorder(
                borderRadius: const BorderRadius.all(
                  const Radius.circular(5.0),
                ),
                borderSide: new BorderSide(
                  color: Colors.black,
                  width: 1.0,
                ),
              ),
              contentPadding: new EdgeInsets.all(10.0),
              filled: true,
              fillColor: Colors.white,
            ),
            onSubmitted: _handleSubmit,
          ),
        ),
      ]
    );
  }
}

class _CustomInput extends StatefulWidget {
  final List<String> listItems;
  final Stream parentStream;

  _CustomInput({
    Key key,
    this.listItems,
    this.parentStream,
  }): super(key: key);

  @override
  State createState() => new _CustomInputState();
}

class HomeState extends State<Home> {
  List<String> _overlayItems = [
    'Item 01',
    'Item 02',
    'Item 03',
  ];
  StreamController _eventDispatcher = new StreamController.broadcast();

  Stream get _stream => _eventDispatcher.stream;

  _onTapUp(TapUpDetails details) {
    _eventDispatcher.add('TAP_UP');
  }

  @override
  void initState() {
    super.initState();
  }

  @override
  void dispose(){
    super.dispose();
    _eventDispatcher.close();
  }

  @override
  Widget build(BuildContext context){
    return new GestureDetector(
      onTapUp: _onTapUp,
      child: new Scaffold(
        appBar: new AppBar(
          title: new Row(
            children: <Widget>[
              new Padding(
                padding: new EdgeInsets.only(
                  right: 10.0,
                ),
                child: new Icon(Icons.layers),
              ),
              new Text(appTitle)
            ],
          ),
          bottom: new PreferredSize(
            preferredSize: const Size.fromHeight(30.0),
            child: new Padding(
              padding: new EdgeInsets.only(
                bottom: 10.0,
                left: 10.0,
                right: 10.0,
              ),
              child: new _CustomInput(
                key: new ObjectKey('$_overlayItems'),
                listItems: _overlayItems,
                parentStream: _stream,
              ),
            ),
          ),
        ),
        body: const Text('Body content'),
      ),
    );
  }
}

class Home extends StatefulWidget {
  @override
  State createState() => new HomeState();
}

void main() => runApp(new MaterialApp(
  title: appTitle,
  home: new Home(),
));

【讨论】:

    【解决方案4】:

    我认为 AppBar 的空间有限。在 AppBar 中放置列表是一种不好的做法。

    【讨论】:

    • 这很公平(关于 AppBar 中的列表)。有没有一种方法可以让小部件存在于 AppBar 中但将小部件添加到正文(或 AppBar 之外的某个地方)?理想情况下,输入将存在于 AppBar 中用于布局目的,并且当用户开始键入时,列表将被添加,并且可以覆盖栏和正文。
    猜你喜欢
    • 2019-04-17
    • 2018-11-19
    • 2021-01-27
    • 1970-01-01
    • 1970-01-01
    • 2021-06-12
    • 1970-01-01
    • 2017-08-10
    • 2021-02-10
    相关资源
    最近更新 更多