我正在制作图片库,我需要用户能够长按图片以显示弹出菜单,以便他删除图片。
到目前为止,我的代码是
<noscript>
生产的产品:
但是,当调用longPress函数时,我也找不到如何完全删除图像的小部件的方法。怎么做?
答案 0 :(得分:5)
OP和第一应答者使用PopupMenuButton
绕过了原始问题,在他们的情况下效果很好。但是我认为,关于如何放置自己的菜单以及如何在不使用PopupMenuButton
的情况下如何接收用户响应的更普遍的问题值得回答,因为有时我们希望在自定义小部件上弹出菜单,并且我们希望它不显示在其他手势上(例如,OP的原本意图是长按)。
我着手制作一个简单的应用程序,演示以下内容:
GestureDetector
捕获长按showMenu()
显示一个弹出菜单,并将其放置在手指触摸附近PopupMenuEntry
(经常使用的PopupMenuItem
只能代表一个值)结果是,当您长按一个大黄色区域时,会出现一个弹出菜单,您可以在其中选择+1
或-1
,并且大数字会相应地增加或减少: / p>
跳到最后一整段代码。评论被撒在那里以解释我在做什么。这里有几件事要注意:
showMenu()
的position
参数需要花些力气才能理解。这是RelativeRect
,代表较小的rect在较大的rect内的位置。在我们的情况下,较大的rect是整个屏幕,较小的rect是触摸区域。 Flutter根据以下规则(以简单的英语)放置弹出菜单:
如果较小的rect向较大的rect的左倾斜,则弹出菜单将与较小的rect的左边缘
< / li>如果较小的rect向较大的rect的右倾斜,则弹出菜单将与较小的rect的右边缘
< / li>如果较小的rect位于中间,则哪个边缘获胜取决于语言的文本方向。如果使用英语和其他从左到右的语言,则左边缘将获胜,否则将使用右边缘。
参考PopupMenuButton
's official implementation来查看其如何使用showMenu()
来显示菜单总是有用的。
showMenu()
返回一个Future
。使用Future.then()
注册用于处理用户选择的回调。另一种选择是使用await
。
请记住,PopupMenuEntry
是StatefulWidget
的子类。您可以在其中布置任意数量的子小部件。这就是您在PopupMenuEntry
中表示多个值的方式。如果希望它表示两个值,只需使其包含两个按钮即可,但是您要对其进行布局。
要关闭弹出菜单,请使用Navigator.pop()
。 Flutter将弹出菜单视为较小的“页面”。当我们显示一个弹出菜单时,实际上是在将“页面”推入导航器的堆栈。要关闭弹出菜单,我们从堆栈中弹出菜单,从而完成上述Future
。
这是完整的代码:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Popup Menu Usage',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Popup Menu Usage'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
var _count = 0;
var _tapPosition;
void _showCustomMenu() {
final RenderBox overlay = Overlay.of(context).context.findRenderObject();
showMenu(
context: context,
items: <PopupMenuEntry<int>>[PlusMinusEntry()],
position: RelativeRect.fromRect(
_tapPosition & Size(40, 40), // smaller rect, the touch area
Offset.zero & overlay.size // Bigger rect, the entire screen
)
)
// This is how you handle user selection
.then<void>((int delta) {
// delta would be null if user taps on outside the popup menu
// (causing it to close without making selection)
if (delta == null) return;
setState(() {
_count = _count + delta;
});
});
// Another option:
//
// final delta = await showMenu(...);
//
// Then process `delta` however you want.
// Remember to make the surrounding function `async`, that is:
//
// void _showCustomMenu() async { ... }
}
void _storePosition(TapDownDetails details) {
_tapPosition = details.globalPosition;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
GestureDetector(
// This does not give the tap position ...
onLongPress: _showCustomMenu,
// Have to remember it on tap-down.
onTapDown: _storePosition,
child: Container(
color: Colors.amberAccent,
padding: const EdgeInsets.all(100.0),
child: Text(
'$_count',
style: const TextStyle(
fontSize: 100, fontWeight: FontWeight.bold),
),
),
),
],
),
),
);
}
}
class PlusMinusEntry extends PopupMenuEntry<int> {
@override
double height = 100;
// height doesn't matter, as long as we are not giving
// initialValue to showMenu().
@override
bool represents(int n) => n == 1 || n == -1;
@override
PlusMinusEntryState createState() => PlusMinusEntryState();
}
class PlusMinusEntryState extends State<PlusMinusEntry> {
void _plus1() {
// This is how you close the popup menu and return user selection.
Navigator.pop<int>(context, 1);
}
void _minus1() {
Navigator.pop<int>(context, -1);
}
@override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
Expanded(child: FlatButton(onPressed: _plus1, child: Text('+1'))),
Expanded(child: FlatButton(onPressed: _minus1, child: Text('-1'))),
],
);
}
}
答案 1 :(得分:5)
以 Nick Lee 和hacker1024 的答案为基础,但不是将解决方案转换为mixin,您可以简单地将其转换为小部件:
class PopupMenuContainer<T> extends StatefulWidget {
final Widget child;
final List<PopupMenuEntry<T>> items;
final void Function(T) onItemSelected;
PopupMenuContainer({@required this.child, @required this.items, @required this.onItemSelected, Key key}) : super(key: key);
@override
State<StatefulWidget> createState() => PopupMenuContainerState<T>();
}
class PopupMenuContainerState<T> extends State<PopupMenuContainer<T>>{
Offset _tapDownPosition;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: (TapDownDetails details){
_tapDownPosition = details.globalPosition;
},
onLongPress: () async {
final RenderBox overlay = Overlay.of(context).context.findRenderObject();
T value = await showMenu<T>(
context: context,
items: widget.items,
position: RelativeRect.fromLTRB(
_tapDownPosition.dx,
_tapDownPosition.dy,
overlay.size.width - _tapDownPosition.dx,
overlay.size.height - _tapDownPosition.dy,
),
);
widget.onItemSelected(value);
},
child: widget.child
);
}
}
然后你会像这样使用它:
child: PopupMenuContainer<String>(
child: Image.asset('assets/image.png'),
items: [
PopupMenuItem(value: 'delete', child: Text('Delete'))
],
onItemSelected: (value) async {
if( value == 'delete' ){
await showDialog(context: context, child: AlertDialog(
title: Text('Delete image'),
content: Text('Are you sure you want to delete the image?'),
actions: [
uiFlatButton(child: Text('NO'), onTap: (){ Navigator.of(context).pop(false); }),
uiFlatButton(child: Text('YES'), onTap: (){ Navigator.of(context).pop(true); }),
],
));
}
},
),
调整代码以满足您的需要。
答案 2 :(得分:3)
如果要使用gridView或listview在屏幕上布置图像,则可以使用手势检测器包装每个项目,然后将图像保留在列表中的某个位置,然后只需从列表中删除图像即可并调用setState()。
类似以下内容。 (此代码可能不会编译,但是应该可以告诉您)
ListView.builder(
itemCount: imageList.length,
itemBuilder: (BuildContext context, int index) {
return GestureDetector(
onLongPress: () {
showMenu(
onSelected: () => setState(() => imageList.remove(index))}
items: <PopupMenuEntry>[
PopupMenuItem(
value: this._index,
child: Row(
children: <Widget>[
Icon(Icons.delete),
Text("Delete"),
],
),
)
],
context: context,
);
},
child: imageList[index],
);
}
)
编辑:您也可以使用弹出菜单,如下所示
Container(
margin: EdgeInsets.symmetric(vertical: 10),
height: 100,
width: 100,
child: PopupMenuButton(
child: FlutterLogo(),
itemBuilder: (context) {
return <PopupMenuItem>[new PopupMenuItem(child: Text('Delete'))];
},
),
),
答案 3 :(得分:2)
李小龙(Nick Lee)的答案可以很容易地转换为混合输入,然后可以在要使用弹出菜单的任何地方使用。
mixin:
import 'package:flutter/material.dart' hide showMenu;
import 'package:flutter/material.dart' as material show showMenu;
/// A mixin to provide convenience methods to record a tap position and show a popup menu.
mixin CustomPopupMenu<T extends StatefulWidget> on State<T> {
Offset _tapPosition;
/// Pass this method to an onTapDown parameter to record the tap position.
void storePosition(TapDownDetails details) => _tapPosition = details.globalPosition;
/// Use this method to show the menu.
Future<T> showMenu<T>({
@required BuildContext context,
@required List<PopupMenuEntry<T>> items,
T initialValue,
double elevation,
String semanticLabel,
ShapeBorder shape,
Color color,
bool captureInheritedThemes = true,
bool useRootNavigator = false,
}) {
final RenderBox overlay = Overlay.of(context).context.findRenderObject();
return material.showMenu<T>(
context: context,
position: RelativeRect.fromLTRB(
_tapPosition.dx,
_tapPosition.dy,
overlay.size.width - _tapPosition.dx,
overlay.size.height - _tapPosition.dy,
),
items: items,
initialValue: initialValue,
elevation: elevation,
semanticLabel: semanticLabel,
shape: shape,
color: color,
captureInheritedThemes: captureInheritedThemes,
useRootNavigator: useRootNavigator,
);
}
}
然后使用它:
import 'package:flutter/material.dart';
import './custom_context_menu.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Popup Menu Usage',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Popup Menu Usage'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with CustomPopupMenu {
var _count = 0;
void _showCustomMenu() {
this.showMenu(
context: context,
items: <PopupMenuEntry<int>>[PlusMinusEntry()],
)
// This is how you handle user selection
.then<void>((int delta) {
// delta would be null if user taps on outside the popup menu
// (causing it to close without making selection)
if (delta == null) return;
setState(() {
_count = _count + delta;
});
});
// Another option:
//
// final delta = await showMenu(...);
//
// Then process `delta` however you want.
// Remember to make the surrounding function `async`, that is:
//
// void _showCustomMenu() async { ... }
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
GestureDetector(
// This does not give the tap position ...
onLongPress: _showCustomMenu,
// Have to remember it on tap-down.
onTapDown: storePosition,
child: Container(
color: Colors.amberAccent,
padding: const EdgeInsets.all(100.0),
child: Text(
'$_count',
style: const TextStyle(fontSize: 100, fontWeight: FontWeight.bold),
),
),
),
],
),
),
);
}
}
class PlusMinusEntry extends PopupMenuEntry<int> {
@override
double height = 100;
// height doesn't matter, as long as we are not giving
// initialValue to showMenu().
@override
bool represents(int n) => n == 1 || n == -1;
@override
PlusMinusEntryState createState() => PlusMinusEntryState();
}
class PlusMinusEntryState extends State<PlusMinusEntry> {
void _plus1() {
// This is how you close the popup menu and return user selection.
Navigator.pop<int>(context, 1);
}
void _minus1() {
Navigator.pop<int>(context, -1);
}
@override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
Expanded(child: FlatButton(onPressed: _plus1, child: Text('+1'))),
Expanded(child: FlatButton(onPressed: _minus1, child: Text('-1'))),
],
);
}
}