欢迎访问移动开发之家(rcyd.net),关注移动开发教程。移动开发之家  移动开发问答|  每日更新
页面位置 : > > > 内容正文

autojs模仿QQ长按弹窗菜单实现示例详解二,

来源: 开发者 投稿于  被查看 31910 次 评论:233

autojs模仿QQ长按弹窗菜单实现示例详解二,


目录
  • 引言
  • 弹窗菜单
  • 箭头
  • 如何确认箭头方向?
  • 调整popwindow的位置
    • 调用方法如下
  • 绘制箭头
    • 修改颜色和圆角
      • 给弹框菜单添加点击事件
        • 弹框菜单点击事件引用弹框实例
          • 环境

            引言

            上一节讲了列表和长按事件

            autojs模仿QQ长按弹窗菜单

            弹窗菜单

            由粗到细, 自顶向下的写代码

            我们现在要修改的文件是showMenuWindow.js

            function showMenuWindow(view) {
              let popMenuWindow = ui.inflateXml(
                view.getContext(),
                `
                <column>
                <button id="btn1" text="btn1" />
                </column>
                `,
                null
              );
              let mPopWindow = new PopupWindow(popMenuWindow, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, true);
              mPopWindow.setOutsideTouchable(true);
              mPopWindow.showAsDropDown(view);
            }
            module.exports = showMenuWindow;
            

            我们先修改xml, QQ的弹窗由两部分组成

            • 菜单列表
            • 箭头

            因此, xml如下

            <column>
              <androidx.recyclerview.widget.RecyclerView id="recyclerView" padding="16" layout_width="match_parent" layout_height="match_parent">
              </androidx.recyclerview.widget.RecyclerView>
              <android.view.View id='arrow' ></android.view.View>
            </column>
            

            这给菜单我们用的也是recyclerView, 因此先设置他的adapter, 如果不会就看上一节课程;

            function showMenuWindow(view) {
              let popMenuWindow = ui.inflateXml(
                ...
              );
              setPopMenuRecyclerViewAdapter(popMenuWindow.recyclerView, []);
              ...
            }
            

            设置Adapter的时候, 第一个参数我们是有的, 第二个参数是adapter要绑定的数据, 现在没有;

            这给菜单数据应该有哪些属性呢?

            • 菜单显示的文字
            • 菜单点后的回调函数

            因此, 数据大概是这样的

              menus: [
                {
                  name: "复制",
                  handle: () => {
                    console.log("复制");
                  },
                },
                {
                  name: "分享",
                  handle: () => {
                    console.log("分享");
                  },
                },
              ],
            

            这种可配置的数据, 我们把它放到config.js中.

            数据有了, 接下来我们进入setPopMenuRecyclerViewAdapter方法内部,

            提醒一下, 我是复制黏贴的上一节课的setAdapter方法, 因此设置recyclerview的方法大差不差.

            setPopMenuRecyclerViewAdapter.js

            let definedClass = false;
            const PopMenuRecyclerViewViewHolder = require("./PopMenuRecyclerViewViewHolder");
            const PopMenuRecyclerViewAdapter = require("./PopMenuRecyclerViewAdapter");
            const showMenuWindow = require("../showMenuWindow.js");
            module.exports = async function (recyclerView, items) {
              if (!definedClass) {
                await $java.defineClass(PopMenuRecyclerViewViewHolder);
                await $java.defineClass(PopMenuRecyclerViewAdapter);
                definedClass = true;
              }
              var adapter = new PopMenuRecyclerViewAdapter(items);
              adapter.setLongClick(showMenuWindow);
              recyclerView.setAdapter(adapter);
            };
            

            基本上就是复制黏贴, 修改一下类名即可

            PopMenuRecyclerViewAdapter.js中, 修改一下holderXml即可

            PopMenuRecyclerViewViewHolder.js, bind需要修改

            bind(item) {
              this.itemView.attr("text", item);
              this.item = item;
            }
            

            除了设置adapter, 菜单弹框还需要设置layoutManager, 这样我们可以控制水平方向上菜单的数量

            const layoutManager = new androidx.recyclerview.widget.GridLayoutManager(this, 5);
            grid.setLayoutManager(layoutManager);
            

            先设置layoutManager, 再设置adapter

            PopMenuRecyclerViewViewHolder.js, 需要修改一下bind方法, 他的item是对象, 文本是item.name

            bind(item) {
              this.itemView.attr("text", item.name);
              this.item = item;
            }
            

            运行代码, 看看效果

            菜单出来了, 接着写箭头, 菜单的xml是

            <column>
              <androidx.recyclerview.widget.RecyclerView id="recyclerView" padding="16" layout_width="match_parent" layout_height="match_parent">
              </androidx.recyclerview.widget.RecyclerView>
              <android.view.View id='arrow' ></android.view.View>
            </column>
            

            下面那个View就是我们放箭头的地方

            箭头

            箭头可能指向上方, 也可能指向下方, 我们通过设置View的前景, 来展示箭头

            arrowView.setForeground(drawable);
            

            这里我们要写自己的drawable, 因此, 要继承

            class TriangleDrawable extends android.graphics.drawable.Drawable {}
            

            重写他的draw方法

            draw(canvas) {
              canvas.drawPath(this.path, paint);
            }
            

            画笔创建一支就好, 因为没有发现要创建多支画笔的需求, 以后需要再改, 满足当下即可;

            path肯定够是变的, 因为箭头有上下两个位置;

            那么在这个TriangleDrawable类中, 我们要实现那些东西呢?

            • 设置箭头方向 setDirection
            • 目前想不到别的了

            如何确认箭头方向?

            假设列表有ABC三条数据, ABC依次排列, 在A的顶部, 如果有控件继续放置一条数据D的话,

            那么我们就把弹框菜单放到A的顶部, 如果没有, 就放到A的底部

            怎么判断是否有足够的空间放下D数据呢? 和那些东西有关?

            • 被长按的view的顶部坐标
            • 弹框菜单的高度

            有这两个信息, 我们就可以判断箭头的方向了.

            为了判断箭头方向, 我们新建一个文件, getArrowDirection.js, 文件夹名popMenuRecyclerView, 和箭头明显不合适, 因此我们新建文件夹popMenuArrow

            被长按的view的顶部坐标

            view.getTop()
            

            弹框菜单的高度, 因为弹框还没有显示出来, 所以我们要预先测量他的高度

            popWindow.getContentView().measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
            let popupWindowHeight = popWindow.getContentView().getMeasuredHeight()
            

            判断箭头指向

              if (longClickedViewTop - popupWindowHeight < 0) {
                // 上面放不下了, 菜单在下面出现, 箭头指向上方
                return "up";
              } else {
                return "down";
              }
            

            我们给箭头一个背景色, 先看当前的效果

            可以看到箭头上下的效果已经出来了,

            箭头View的挪动使用了addView和removeView

            let arrowView = popMenuWindow.findView("arrow");
            popMenuWindow.findView("root").removeView(arrowView);
            popMenuWindow.findView("root").addView(arrowView, 0);
            

            这里有个问题, 箭头的背景色为什么那么长, 是弹框菜单的两倍多.

            这是因为GridLayoutManager第二个参数设置了5, 我们改为Math.min, 取最小值, 宽度问题就符合预期了

            const layoutManager = new GridLayoutManager(view.getContext(), Math.min(popMenus.length, 5));
            

            调整popwindow的位置

            如果弹框菜单在长按控件的上方, 那么应该偏移多少?

            Y轴偏移量 = 弹框菜单的高度 + 长按控件的高度

            调用方法如下

                let offset = popMenuCalculateOffset(view, mPopWindow, arrowDirection);
                if (arrowDirection == "down") {
                  console.log("箭头朝下");
                  mPopWindow.showAsDropDown(view, offset.x, offset.y);
                }
            

            我们新建一个文件 popMenuCalculateOffset.js

            module.exports = function popMenuCalculateOffset(longClickedView, popWindow, arrowDirection) {
              let contentView = popWindow.getContentView();
              let width = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
              let height = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
              contentView.measure(width, height);
              popWindow.setBackgroundDrawable(new ColorDrawable(0));
              let contentViewHeight = contentView.getMeasuredHeight();
              let longClickedViewHeight = longClickedView.getHeight();
              console.log("contentViewHeight = " + contentViewHeight);
              if (arrowDirection == "down") {
                let y = contentViewHeight + longClickedViewHeight;
                return { x: 0, y: -y };
              } else {
                return { x: 0, y: 0 };
              }
            };
            

            获取高宽高以后, 我们的

                let offset = popMenuCalculateOffset(view, mPopWindow, arrowDirection);
                if (arrowDirection == "down") {
                  console.log("箭头朝下");
                  mPopWindow.showAsDropDown(view, offset.x, offset.y);
                } else {
                  let arrowView = popMenuWindow.findView("arrow");
                  popMenuWindow.findView("root").removeView(arrowView);
                  popMenuWindow.findView("root").addView(arrowView, 0);
                  mPopWindow.showAsDropDown(view, offset.x, offset.y);
                }
            

            代码写了不少了, 看看效果, 及时排查bug

            箭头朝上

            箭头朝下

            绘制箭头

            我们用canvas画个三角形, 首先我们要继承类, 重写他的draw方法

            class TriangleDrawable extends android.graphics.drawable.Drawable {}
            

            单独写一个类文件 TriangleDrawable.js, 放到文件夹 popMenuArrow;

            绘制箭头之前, 要知道箭头的宽高, 和箭头的中点;

            • 箭头的宽高, 我们就用arrowView的高度;
            • 箭头的中点, 我们指向被长按的控件 X 轴的中心

            为了使类, 尽可能的比较纯, 我们传递的参数选择具体的数值, 而不是控件;

            这里的纯指的是没有副作用, 以及可复用的程度

            class TriangleDrawable extends android.graphics.drawable.Drawable {
              setHeight(height) {
                this.height = height;
              }
              setWidth(width) {
                this.width = width;
              }
              setDirection(direction) {
                this.direction = direction;
              }
              setColor(color) {
                this.color = Color.parse(color).value;
              }
              setLongClickedViewWidth(longClickedViewWidth) {
                this.longClickedViewWidth = longClickedViewWidth;
              }
              draw(canvas) {
                trianglePath.reset();
                if (this.direction == "down") {
                  console.log("down");
                  trianglePath.moveTo(this.width / 2, this.height);
                  trianglePath.lineTo(this.width / 2 - this.height / 2, 0);
                  trianglePath.lineTo(this.width / 2 + this.height / 2, 0);
                } else {
                  trianglePath.moveTo(this.width / 2, 0);
                  trianglePath.lineTo(this.width / 2 - this.height / 2, this.height);
                  trianglePath.lineTo(this.width / 2 + this.height / 2, this.height);
                }
                trianglePath.close();
                canvas.drawPath(trianglePath, paint);
              }
            }
            module.exports = TriangleDrawable;
            

            在popupWindow出现之前, 我们要把箭头绘制出来,

            await setArrowForeground(arrow, arrowDirection, view);
            mPopWindow.showAsDropDown(view, offset.x, offset.y);
            

            使用onPreDraw, 在绘制之前, 我们可以获取到正确的宽高

              arrow.getViewTreeObserver().addOnPreDrawListener(
                new android.view.ViewTreeObserver.OnPreDrawListener({
                  onPreDraw: function () {
                    arrow.getViewTreeObserver().removeOnPreDrawListener(this);
                    let arrowHeight = arrow.getHeight();
                    let arrowWidth = arrow.getWidth();
                    triangleDrawable.setWidth(arrowWidth);
                    triangleDrawable.setHeight(arrowHeight);
                    arrow.setForeground(triangleDrawable);
                    return true;
                  },
                })
              );
            

            代码写了不少了, 先测试一下效果

            箭头朝上

            箭头朝下

            修改颜色和圆角

            颜色这个就不多说了, 非常容易修改, 说下圆角

            修改圆角是在这个文件中: showMenuWindow.js, 我们要给RecyclerView包裹一层card

            <card cardCornerRadius="8dp" w='wrap_content'>
            ...
            </card>
            

            给弹框菜单添加点击事件

            也就是给弹框菜单中的recyclerview添加点击事件

            增加点击事件所在的文件是 popMenuRecyclerView/PopMenuRecyclerViewAdapter.js,

            我们修改他的onCreateViewHolder

            onCreateViewHolder(parent) {
              let testRecyclerViewViewHolder = new PopMenuRecyclerViewViewHolder(ui.inflateXml(parent.getContext(), holderXml, parent));
              testRecyclerViewViewHolder.itemView.setOnClickListener(() => {
                let item = this.data[testRecyclerViewViewHolder.getAdapterPosition()];
                item.handle();
                return true;
              });
              return testRecyclerViewViewHolder;
            }
            

            点击事件生效了, 还有个问题, 点击了之后,弹框菜单没有消失, 我们在这里又引用不到弹框实例, 怎么弄?

            弹框菜单点击事件引用弹框实例

            我们可以用全局对象, 挂载弹框的实例;

            我们不选怎全局对象, 而是去能引用的地方引用实例;

            在 showMenuWindow.js 这个文件中, 出现了popupWindow实例, 我们把这个实例作为参数, 传递给

            setPopMenuRecyclerViewAdapter

            setPopMenuRecyclerViewAdapter(mPopWindow, grid, popMenus);
            

            setPopMenuRecyclerViewAdapter.js

            module.exports = async function (mPopWindow, recyclerView, items) {
              const menuClick = (item, itemView) => {
                console.log(itemView);
                item.handle();
                mPopWindow.dismiss();
              };
              var adapter = new PopMenuRecyclerViewAdapter(items);
              adapter.setClick(menuClick);
              recyclerView.setAdapter(adapter);
            };
            

            我们在这个文件中给adapter设置了点击事件, 相应的要在 PopMenuRecyclerViewAdapter.js 文件中添加方法,

            setClick

            class PopMenuRecyclerViewAdapter extends androidx.recyclerview.widget.RecyclerView.Adapter {
              constructor(data) {
                super();
                this.data = data;
                this.click = () => {};
              }
              onCreateViewHolder(parent) {
                let testRecyclerViewViewHolder = new PopMenuRecyclerViewViewHolder(ui.inflateXml(parent.getContext(), holderXml, parent));
                testRecyclerViewViewHolder.itemView.setOnClickListener(() => {
                  let item = this.data[testRecyclerViewViewHolder.getAdapterPosition()];
                  this.click(item, testRecyclerViewViewHolder.itemView);
                  return true;
                });
                return testRecyclerViewViewHolder;
              }
              ...
              setClick(click) {
                this.click = click;
              }
            }
            module.exports = PopMenuRecyclerViewAdapter;
            

            到这里就模仿的差不多了, 差不多就行.

            如果要增加多个菜单, 在config.js中修改配置即可

            环境

            设备: 小米11pro
            Android版本: 12
            Autojs版本: 9.3.11

            名人名言

            思路是最重要的, 其他的百度, bing, stackoverflow, github, 安卓文档, autojs文档, 最后才是群里问问 --- 牙叔教程

            声明

            部分内容来自网络 本教程仅用于学习, 禁止用于其他用途

            以上就是autojs模仿QQ长按弹窗菜单实现示例详解二的详细内容,更多关于autojs模仿QQ长按弹窗菜单的资料请关注3672js教程其它相关文章!

            您可能感兴趣的文章:
            • autojs模仿QQ长按弹窗菜单实现示例
            • autojs使用intent发送邮件带附件实现示例
            • 详解autojs的nodejs编写UI技巧示例
            • autojs的nodejs打包成品app经验分享
            • Android autojs随时翻译剪贴板单词实现示例

            用户评论