Flutter-长按PopupMenu

时间:2019-01-22 01:40:01

标签: dart flutter

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

到目前为止,我的代码是

<noscript>

生产的产品:

但是,当调用longPress函数时,我也找不到如何完全删除图像的小部件的方法。怎么做?

4 个答案:

答案 0 :(得分:5)

OP和第一应答者使用PopupMenuButton绕过了原始问题,在他们的情况下效果很好。但是我认为,关于如何放置自己的菜单以及如何在不使用PopupMenuButton 的情况下如何接收用户响应的更普遍的问题值得回答,因为有时我们希望在自定义小部件上弹出菜单,并且我们希望它不显示在其他手势上(例如,OP的原本意图是长按)。

我着手制作一个简单的应用程序,演示以下内容:

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

结果是,当您长按一个大黄色区域时,会出现一个弹出菜单,您可以在其中选择+1-1,并且大数字会相应地增加或减少: / p>

Popup Menu Usage App

跳到最后一整段代码。评论被撒在那里以解释我在做什么。这里有几件事要注意:

  1. showMenu()position参数需要花些力气才能理解。这是RelativeRect,代表较小的rect在较大的rect内的位置。在我们的情况下,较大的rect是整个屏幕,较小的rect是触摸区域。 Flutter根据以下规则(以简单的英语)放置弹出菜单:

    • 如果较小的rect向较大的rect的倾斜,则弹出菜单将与较小的rect的左边缘

      < / li>
    • 如果较小的rect向较大的rect的倾斜,则弹出菜单将与较小的rect的右边缘

      < / li>
    • 如果较小的rect位于中间,则哪个边缘获胜取决于语言的文本方向。如果使用英语和其他从左到右的语言,则左边缘将获胜,否则将使用右边缘。

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