Flutter -长按弹出菜单

clj7thdc  于 2023-01-27  发布在  Flutter
关注(0)|答案(5)|浏览(636)

我正在制作一个图片库,我需要用户能够长按一个图像显示一个弹出菜单,这将让他删除图像。
我的代码,到目前为止:

return GestureDetector(
    onLongPress: () {
      showMenu(
        items: <PopupMenuEntry>[
          PopupMenuItem(
            value: this._index,
            child: Row(
              children: <Widget>[
                Icon(Icons.delete),
                Text("Delete"),
              ],
            ),
          )
        ],
        context: context,
      );
    },
    child: Image.memory(
      this._asset.thumbData.buffer.asUint8List(),
      fit: BoxFit.cover,
      gaplessPlayback: true,
    ),
  );

它产生:

但是,我也不知道如何在调用longPress函数时完全删除图像的小部件。

k0pti3hp

k0pti3hp1#

OP和第一个回答者使用PopupMenuButton绕过了最初的问题,在他们的案例中运行良好。但是我认为更普遍的问题是,如何定位自己的菜单以及如何接收用户的响应 * 而不使用PopupMenuButton* 是值得回答的,因为有时我们希望在自定义小部件上有一个弹出菜单,我们希望它出现在一些手势上,而不是简单的点击(例如,OP的最初意图是长按)。
我开始制作一个简单的应用程序,演示以下内容:
1.使用GestureDetector捕获长按
1.使用函数showMenu()显示弹出菜单,并将其放置在手指触摸的位置附近
1.如何接收用户的选择
1.如何创建一个表示多个值的PopupMenuEntry(常用的PopupMenuItem只能表示一个值)
结果是,当你长按一个大的黄色区域时,会出现一个弹出菜单,你可以在上面选择+1-1,大的数字会相应地增加或减少:

跳到代码的末尾,查看整个代码主体。注解被分散在那里,用来解释我正在做什么。这里有一些需要注意的事情:

  • 要理解showMenu()position参数还需要一些努力。它是一个RelativeRect,表示如何将较小的矩形放置在较大的矩形中。在我们的示例中,较大的矩形是整个屏幕,较小的矩形是触摸区域。Flutter根据以下规则(简单英语)定位弹出菜单:
  • 如果较小的矩形倾斜向较大矩形的一半,则弹出菜单将与较小矩形的左边缘对齐
  • 如果较小的矩形倾斜向较大矩形的一半,则弹出菜单将与较小矩形的右边缘对齐
  • 如果较小的矩形在中间,则哪一边获胜取决于语言的文本方向。如果使用英语和其他从左到右的语言,则左边缘获胜,否则右边缘获胜。

引用PopupMenuButton 's official implementation来查看它如何使用showMenu()来显示菜单总是很有用的。

  1. showMenu()返回Future。使用Future.then()注册回调以处理用户选择。另一个选项是使用await
    1.记住PopupMenuEntryStatefulWidget的一个子类,你可以在其中放置任意数量的子部件,这就是你在PopupMenuEntry中表示多个值的方法,如果你想让它表示两个值,只要让它包含两个按钮,不管你想怎么放置它们。
    1.要关闭弹出菜单,请使用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 & const 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'))),
      ],
    );
  }
}
qnakjoqk

qnakjoqk2#

如果您打算使用gridView或listview在屏幕上布局图像,您可以使用手势检测器 Package 每个项目,然后您应该将图像保存在列表中的某个位置,然后只需从列表中删除图像并调用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'))];
    },
  ),
),
w8f9ii69

w8f9ii693#

以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); }),
        ],
      ));
    }
  },
),

根据需要调整代码。

c0vxltue

c0vxltue4#

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'))),
      ],
    );
  }
}
unguejic

unguejic5#

2023年的答案
Flutter 3.7中,现在有一个ContextMenuRegion小部件,你可以把它 Package 在任何现有的小部件上,当用户长按或右键单击(取决于平台)时,你给予它的菜单就会出现。

return Scaffold(                                        
   body: Center(                                         
     child: ContextMenuRegion(                           
       contextMenuBuilder: (context, offset) {           
         return AdaptiveTextSelectionToolbar.buttonItems(
           anchors: TextSelectionToolbarAnchors(         
             primaryAnchor: offset,                      
           ),                                             
           buttonItems: <ContextMenuButtonItem>[         
             ContextMenuButtonItem(                      
               onPressed: () {                           
                 ContextMenuController.removeAny();      
               },                                        
               label: 'Save',                            
             ),                                           
           ],                                             
         );                                               
       },                                                
       child: const SizedBox(                            
         width: 200.0,                                   
         height: 200.0,                                  
         child: FlutterLogo(),                           
       ),                                                 
     ),                                                   
   ),                                                     
 );

相关问题