据我所知,这应该是可能的,但我不太能让它工作。我在appBar
的bottom
中有一个Stack
,在Stack
中有一个Positioned
列表。一切似乎都在预期的位置,但appBar
正在裁剪列表,因此如果appBar
和body
的内容,列表不会显示在顶部。
我刚开始接触Flutter,但在HTML界,我会有一个绝对定位的列表,并且appBar会被固定为一个高于正文的z索引,从而实现分层效果。
我已经尝试了一大堆变体,但看起来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(
发布于 2018-07-15 02:05:24
创建了一个示例文件,演示了我想出的内容(至少与这个问题相关的内容)。希望它能将其他人从不必要的麻烦中解救出来。
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-09 14:05:04
我从未对此进行过测试,但AppBar有一个接受小部件作为参数的flexibleSpace
属性。此小部件被放置在AppBar顶部(标题所在的位置)和AppBar底部之间的空间中。如果您将您的小部件放在这个空间中,而不是AppBar的底部(应该只用于TabBars等小部件),您的应用程序可能会正常工作。
另一种可能的解决方案是将列表元素放在DropdownButton中,而不是Stack中。
您可以在AppBar here上找到更多信息。
编辑:您还可以考虑在使用搜索时使用脚手架主体来显示建议。
此外,您可能会发现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-09 22:53:12
您将无法使用Positioned
小部件来绝对定位剪辑外部的内容。( AppBar要求此剪辑遵循材质规格,因此它可能不会更改)。
如果您需要将某些东西定位在构建它的小部件的边界之外,那么您需要一个Overlay
。覆盖图本身是由MaterialApp
中的导航器创建的,因此您可以将新元素推入其中。其他一些使用Overlay
的小部件是工具提示和弹出菜单,因此如果您愿意,可以查看它们的实现以获得更多灵感。
final OverlayEntry entry = new OverlayEntry(builder: (BuildContext context) => /* ... */)
Overlay.of(context, debugRequiredFor: widget).insert(_entry);
https://stackoverflow.com/questions/51237977
复制相似问题