如何在 AppBar 中使用 Positioned 小部件
How to use a Positioned widget in an AppBar
根据我的阅读,这应该是可行的,但我不能完全让它工作。我在 appBar
的 bottom
中有一个 Stack
,在 Stack
中有一个 Positioned
列表。一切似乎都按预期定位,但 appBar
正在裁剪列表,因此如果 appBar
和 body
的内容,列表不会显示在顶部。
我是 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(),
));
根据我的阅读,这应该是可行的,但我不能完全让它工作。我在 appBar
的 bottom
中有一个 Stack
,在 Stack
中有一个 Positioned
列表。一切似乎都按预期定位,但 appBar
正在裁剪列表,因此如果 appBar
和 body
的内容,列表不会显示在顶部。
我是 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(),
));