如何在颤动的按下/手指/鼠标/光标位置显示菜单

Posted

技术标签:

【中文标题】如何在颤动的按下/手指/鼠标/光标位置显示菜单【英文标题】:How to show a menu at press/finger/mouse/cursor position in flutter 【发布时间】:2019-06-14 06:31:57 【问题描述】:

我有这段从Style clipboard in flutter得到的代码

showMenu(
        context: context,
        // TODO: Position dynamically based on cursor or textfield
        position: RelativeRect.fromLTRB(0.0, 600.0, 300.0, 0.0),
        items: [
          PopupMenuItem(
            child: Row(
              children: <Widget>[
                // TODO: Dynamic items / handle click
                PopupMenuItem(
                  child: Text(
                    "Paste",
                    style: Theme.of(context)
                        .textTheme
                        .body2
                        .copyWith(color: Colors.red),
                  ),
                ),
                PopupMenuItem(
                  child: Text("Select All"),
                ),
              ],
            ),
          ),
        ],
      );

此代码效果很好,除了创建的弹出窗口位于固定位置之外,我将如何使其弹出在鼠标/按下/手指/光标位置或附近的某个位置,有点像当您想在手机上复制和粘贴。 (此对话框弹出不会用于复制和粘贴)

【问题讨论】:

【参考方案1】:

像这样使用手势检测器的 onTapDown

 GestureDetector(
        onTapDown: (TapDownDetails details) 
          showPopUpMenu(details.globalPosition);
        ,

然后在这种方法中,我们使用点击详细信息来查找位置

Future<void> showPopUpMenu(Offset globalPosition) async 
double left = globalPosition.dx;
double top = globalPosition.dy;
await showMenu(
  color: Colors.white,
  //add your color
  context: context,
  position: RelativeRect.fromLTRB(left, top, 0, 0),
  items: [
    PopupMenuItem(
      value: 1,
      child: Padding(
        padding: const EdgeInsets.only(left: 0, right: 40),
        child: Row(
          children: [
            Icon(Icons.mail_outline),
            SizedBox(
              width: 10,
            ),
            Text(
              "Menu 1",
              style: TextStyle(color: Colors.black),
            ),
          ],
        ),
      ),
    ),
    PopupMenuItem(
      value: 2,
      child: Padding(
        padding: const EdgeInsets.only(left: 0, right: 40),
        child: Row(
          children: [
            Icon(Icons.***_key),
            SizedBox(
              width: 10,
            ),
            Text(
              "Menu 2",
              style: TextStyle(color: Colors.black),
            ),
          ],
        ),
      ),
    ),
    PopupMenuItem(
      value: 3,
      child: Row(
        children: [
          Icon(Icons.power_settings_new_sharp),
          SizedBox(
            width: 10,
          ),
          Text(
            "Menu 3",
            style: TextStyle(color: Colors.black),
          ),
        ],
      ),
    ),
  ],
  elevation: 8.0,
).then((value) 
  print(value);
  if (value == 1) 
    //do your task here for menu 1
  
  if (value == 2) 
    //do your task here for menu 2
  
  if (value == 3) 
    //do your task here for menu 3
  
);

希望有效果

【讨论】:

【参考方案2】:

我可以通过使用这个答案来解决类似的问题: https://***.com/a/54714628/559525

基本上,我在每个 ListTile 周围添加了一个 GestureDetector(),然后您使用 onTapDown 存储您的新闻位置并使用 onLongPress 调用您的 showMenu 函数。以下是我添加的关键函数:

  _showPopupMenu() async 
    final RenderBox overlay = Overlay.of(context).context.findRenderObject();

    await showMenu(
      context: context,
      position: RelativeRect.fromRect(
          _tapPosition & Size(40, 40), // smaller rect, the touch area
          Offset.zero & overlay.size // Bigger rect, the entire screen
          ),
      items: [
        PopupMenuItem(
          child: Text("Show Usage"),
        ),
        PopupMenuItem(
          child: Text("Delete"),
        ),
      ],
      elevation: 8.0,
    );
  

  void _storePosition(TapDownDetails details) 
    _tapPosition = details.globalPosition;
  

然后这里是完整的代码(你必须调整一些东西,比如图像,并填写设备列表):

import 'package:flutter/material.dart';
import 'package:auto_size_text/auto_size_text.dart';
import 'dart:core';

class RecentsPage extends StatefulWidget 
  RecentsPage(Key key, this.title) : super(key: key);

  final String title;

  @override
  _RecentsPageState createState() => _RecentsPageState();


class _RecentsPageState extends State<RecentsPage> 

  List<String> _recents;

  var _tapPosition;

  @override
  void initState() 
    super.initState();
    _tapPosition = Offset(0.0, 0.0);
    getRecents().then((value) 
      setState(() 
        _recents = value;
      );
    );
  

  @override
  Widget build(BuildContext context) 
    return Scaffold(
      backgroundColor: Color(0xFFFFFFFF),
      body: SafeArea(
        child: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.start,
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: <Widget>[
              Container(height: 25),
              Stack(
                children: <Widget>[
                  Container(
                    padding: EdgeInsets.only(left: 40),
                    child: Center(
                      child: AutoSizeText(
                        "Recents",
                        maxLines: 1,
                        textAlign: TextAlign.center,
                        style: TextStyle(fontSize: 32),
                      ),
                    ),
                  ),
                  Container(
                    padding: EdgeInsets.only(left: 30, top: 0),
                    child: GestureDetector(
                        onTap: () => Navigator.of(context).pop(),
                        child: Transform.scale(
                          scale: 2.0,
                          child: Icon(
                            Icons.chevron_left,
                          ),
                        )),
                  ),
                ],
              ),
              Container(
                height: 15,
              ),
              Container(
                height: 2,
                color: Colors.blue,
              ),
              Container(
                height: 10,
              ),
              Flexible(
                child: ListView(
                  padding: EdgeInsets.all(15.0),
                  children: ListTile.divideTiles(
                    context: context,
                    tiles: _getRecentTiles(),
                  ).toList(),
                ),
              ),
              Container(height: 15),
            ],
          ),
        ),
      ),
    );
  

  List<Widget> _getRecentTiles() 
    List<Widget> devices = List<Widget>();
    String _dev;
    String _owner = "John Doe";

    if (_recents != null) 
      for (_dev in _recents.reversed) 
        if (_dev != null) 
          _dev = _dev.toUpperCase().trim();

            String serial = "12341234";

            devices.add(GestureDetector(
                onTapDown: _storePosition,
                onLongPress: () 
                  print("long press of $serial");
                  _showPopupMenu();
                ,
                child: ListTile(
                  contentPadding: EdgeInsets.symmetric(vertical: 20),
                  leading: Transform.scale(
                      scale: 0.8,
                      child: Image(
                        image: _myImage,
                      )),
                  title: AutoSizeText(
                    "$_owner",
                    maxLines: 1,
                    style: TextStyle(fontSize: 22),
                  ),
                  subtitle: Text("Serial #: $serial"),
                  trailing: Icon(Icons.keyboard_arrow_right),
                )));
        
      
     else 
      devices.add(ListTile(
        contentPadding: EdgeInsets.symmetric(vertical: 20),
        title: AutoSizeText(
          "No Recent Devices",
          maxLines: 1,
          style: TextStyle(fontSize: 20),
        ),
        subtitle:
            Text("Click the button to add a device"),
        onTap: () 
          print('add device');
        ,
      ));
    
    return devices;
  

  _showPopupMenu() async 
    final RenderBox overlay = Overlay.of(context).context.findRenderObject();

    await showMenu(
      context: context,
      position: RelativeRect.fromRect(
          _tapPosition & Size(40, 40), // smaller rect, the touch area
          Offset.zero & overlay.size // Bigger rect, the entire screen
          ),
      items: [
        PopupMenuItem(
          child: Text("Show Usage"),
        ),
        PopupMenuItem(
          child: Text("Delete"),
        ),
      ],
      elevation: 8.0,
    );
  

  void _storePosition(TapDownDetails details) 
    _tapPosition = details.globalPosition;
  

【讨论】:

我得到了空错误,因为 _tapPosition 甚至在菜单显示之前也没有被初始化。只需将其设置为 _tapPosition = Offset(0.0, 0.0);在构建函数之外,它不会出错,并且会在调用 onTapDown 时更新到新位置。 对不起,我从来没有得到那个,但是是的,你可以将它写入 initState() 中的 Offset(0.0, 0.0) ,例如像这样的有状态小部件。感谢您的评论! 如何设置_tapPosition = Offset(0.0, 0.0);我想根据不同手机的屏幕尺寸(宽度)将菜单位置设置在屏幕的右上角?请建议。谢谢 @Kamlesh 首先,在这个有状态的小部件开始时将 _tapPosition 设置为 0,0 是将其放入 initState() 函数中。我更新了上面的答案以包括那一行。但听起来你想要一些不同的东西,比如总是在任何尺寸屏幕的右上角显示弹出窗口?在这种情况下,其他帖子可能会为您提供更多帮助:***.com/questions/61756271/… offset: const Offset(0, -380),帮助了我。谢谢【参考方案3】:

这是一个可重复使用的小部件,可以满足您的需要。只需用这个CopyableWidget 包裹你的Text 或其他Widget 并传入onGetCopyTextRequested。长按小部件时将显示复制菜单,将返回的文本内容复制到剪贴板,并在完成时显示Snackbar

/// The text to copy to the clipboard should be returned or null if nothing can be copied
typedef GetCopyTextCallback = String Function();

class CopyableWidget extends StatefulWidget 

    final Widget child;
    final GetCopyTextCallback onGetCopyTextRequested;

    const CopyableWidget(
        Key key,
        @required this.child,
        @required this.onGetCopyTextRequested,
    ) : super(key: key);

    @override
    _CopyableWidgetState createState() => _CopyableWidgetState();


class _CopyableWidgetState extends State<CopyableWidget> 

    Offset _longPressStartPos;

    @override
    Widget build(BuildContext context) 
        return InkWell(
            highlightColor: Colors.transparent,
            onTapDown: _onTapDown,
            onLongPress: () => _onLongPress(context),
            child: widget.child
        );
    

    void _onTapDown(TapDownDetails details) 
        setState(() 
            _longPressStartPos = details?.globalPosition;
        );
    

    void _onLongPress(BuildContext context) async 
        if (_longPressStartPos == null)
            return;

        var isCopyPressed = await showCopyMenu(
            context: context,
            pressedPosition: _longPressStartPos
        );
        if (isCopyPressed == true && widget.onGetCopyTextRequested != null) 
            var copyText = widget.onGetCopyTextRequested();
            if (copyText != null) 
                await Clipboard.setData(ClipboardData(text: copyText));

                _showSuccessSnackbar(
                    context: context,
                    text: "Copied to the clipboard"
                );
            
        
    

    void _showSuccessSnackbar(
        @required BuildContext context,
        @required String text
    ) 
        var scaffold = Scaffold.of(context, nullOk: true);
        if (scaffold != null) 
            scaffold.showSnackBar(
                SnackBar(
                    content: Row(
                        children: <Widget>[
                            Icon(
                                Icons.check_circle_outline,
                                size: 24,
                            ),
                            SizedBox(width: 8),
                            Expanded(
                                child: Text(text)
                            )
                        ],
                    )
                )
            );
        
    


Future<bool> showCopyMenu(
    BuildContext context,
    Offset pressedPosition
) 
    var x = pressedPosition.dx;
    var y = pressedPosition.dy;

    return showMenu<bool>(
        context: context,
        position: RelativeRect.fromLTRB(x, y, x + 1, y + 1),
        items: [
            PopupMenuItem<bool>(value: true, child: Text("Copy")),
        ]
    );

【讨论】:

以上是关于如何在颤动的按下/手指/鼠标/光标位置显示菜单的主要内容,如果未能解决你的问题,请参考以下文章

苹果笔记本丢失鼠标指针

Expression Blend学习5控件

如何在文本字段中颤动的值末尾设置光标位置?

鼠标左键按下时如何更改鼠标光标?

如何在 QML 中设置鼠标光标位置

vc 如何判断鼠标光标在某个矩形框内,如果在,显示一个子窗口