【问题标题】:Flutter - PopupMenu on long pressFlutter - 长按弹出菜单
【发布时间】:2019-06-15 10:33:41
【问题描述】:

我正在制作一个图片库,我需要用户能够长按图片以显示一个弹出菜单,让他删除图片。

我的代码,到目前为止:

  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 函数时,我不知道如何完全删除图像的小部件。该怎么做?

【问题讨论】:

    标签: dart flutter


    【解决方案1】:

    OP 和 First Answerer 使用 PopupMenuButton 绕过了原始问题,这在他们的情况下运行良好。但我认为如何定位自己的菜单以及如何在不使用PopupMenuButton 的情况下接收用户响应的更一般的问题值得回答,因为有时我们希望在自定义小部件上弹出菜单,并且我们希望它出现在一些手势上,而不是简单的点击(例如,OP 的初衷是长按)。

    我开始制作一个简单的应用程序来演示以下内容:

    1. 使用GestureDetector 捕获长按
    2. 使用函数showMenu()显示弹出菜单,并将其放置在手指触摸附近
    3. 如何接收用户的选择
    4. (Bonus)如何制作一个代表多个值的PopupMenuEntry(常用的PopupMenuItem只能代表一个值)

    结果是,当你长按一个大的黄色区域时,会出现一个弹出菜单,你可以在上面选择+1-1,大数字会相应地增加或减少:

    整个代码体跳到最后。评论被洒在那里解释我在做什么。这里有几点需要注意:

    1. showMenu()position 参数需要花点功夫才能理解。这是一个RelativeRect,它代表一个较小的矩形是如何定位在一个较大的矩形内的。在我们的例子中,较大的矩形是整个屏幕,较小的矩形是触摸区域。 Flutter 根据这些规则定位弹出菜单(用简单的英语):

      • 如果较小的矩形向较大矩形的一半倾斜,弹出菜单将与较小矩形的左边缘对齐

      • 如果较小的矩形向较大矩形的一半倾斜,弹出菜单将与较小矩形的右边缘对齐

      • 如果较小的矩形在中间,则哪个边获胜取决于语言的文本方向。如果使用英语和其他从左到右的语言,左边缘获胜,否则右边缘获胜。

    参考PopupMenuButton's official implementation 了解它如何使用showMenu() 显示菜单总是有用的。

    1. showMenu() 返回一个Future。使用Future.then() 注册回调以处理用户选择。另一种选择是使用await

    2. 请记住,PopupMenuEntryStatefulWidget 的(子类)。您可以在其中布局任意数量的子小部件。这就是您在PopupMenuEntry 中表示多个值的方式。如果你想让它代表两个值,只要让它包含两个按钮,不管你想布局它们。

    3. 要关闭弹出菜单,请使用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'))),
          ],
        );
      }
    }
    

    【讨论】:

    • 谢谢你。自最初发布以来的一次小更新。 overlay.size 现在是 overlay.semanticBounds.size
    • 谢谢@Graham。但是你能指出我的财产的参考页吗?没跟上,也找不到了。
    • 是的,去这里api.flutter.dev/flutter/rendering/RenderBox-class.html。然后向下滚动到semanticBounds 属性。然后单击它旁边的Rect,它将带您进入Rect 课程。然后向下滚动到size 属性。从文档看来,RenderBox 也有一个 size 属性,但是当我尝试将它与 Flutter v1.5.4 一起使用时出现编译错误。
    • 谢谢你。这真的很有用,而且我很难理解 Flutter 中菜单的实现方式与 ListView.builder 的关系。
    • 非常感谢,我没有像 avil12 这样的问题,但是你的工具的神奇“.then()”帮助了我。我需要在成功打开弹​​出菜单后如何检测用户选择器。
    【解决方案2】:

    如果您打算使用 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'))];
        },
      ),
    ),
    

    【讨论】:

    • 我可以删除图像,但仍然无法正确定位菜单,并且在单击“删除”时 - 菜单仍然存在。如何解决?
    • 我认为使用 PopupMenuButton 可以解决您的两个问题。它需要一个小部件作为子部件,因此您可以将图像放入其中。
    • 问题是我不能在showMenu(...) 中使用PopupMenuButton,因为它不是PopupMenuEntry 的实例
    • 如果可以使用弹出菜单按钮(将图像包裹在其中),则无需使用 showMenu。它将处理弹出窗口的显示,只要您的所有菜单项的值不是 null,它就会在选择项目时关闭弹出窗口。
    • @avi12 编辑了我的答案,包括一个如何使用弹出菜单按钮的示例
    【解决方案3】:

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

    调整代码以满足您的需求。

    【讨论】:

      【解决方案4】:

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

      【讨论】:

        猜你喜欢
        • 2017-11-07
        • 2011-08-22
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-06-13
        • 2021-08-12
        相关资源
        最近更新 更多