1

从我读过的内容来看,这应该是可能的,但我无法让它发挥作用。我有一个Stack里面的bottomappBar里面有一个Positioned列表Stack。一切似乎都按预期定位,但是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(
4

4 回答 4

1

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

在此处输入图像描述

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(),
));
于 2018-07-14T18:05:23.783 回答
1

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

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

final OverlayEntry entry = new OverlayEntry(builder: (BuildContext context) => /* ... */)
Overlay.of(context, debugRequiredFor: widget).insert(_entry);
于 2018-07-09T14:53:12.257 回答
1

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

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

您可以在此处找到有关 AppBar 的更多信息。

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

此外,您可能会发现 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);
    });
  }
于 2018-07-09T06:05:04.123 回答
0

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

于 2018-07-09T05:11:58.293 回答