如何在 AppBar 中使用 Positioned 小部件

How to use a Positioned widget in an AppBar

根据我的阅读,这应该是可行的,但我不能完全让它工作。我在 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(

我认为 AppBar 有一个限制 space。将列表放在 AppBar 中是一种不好的做法。

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

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

您可以在 AppBar 上找到更多信息 here

编辑: 您还可以考虑使用脚手架 body 在使用搜索时显示建议。

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

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

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

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

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

创建了一个示例文件来演示我的想法(至少是与这个问题相关的内容)。希望它能让其他人免于任何不必要的麻烦。

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