autojs模仿QQ长按弹窗菜单(二)

简介: 这个菜单数据应该有哪些属性呢?​菜单显示的文字菜单点后的回调函数因此, 数据大概是这样的

牙叔教程 简单易懂

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

https://www.yuque.com/yashujs/bfug6u/ptqpaybz649tpr8h

今天讲弹窗菜单

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

我们现在要修改的文件是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文档, 最后才是群里问问 --- 牙叔教程


声明


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


相关文章
|
Android开发
autojs按钮不可点击
牙叔教程 简单易懂
1121 0
|
JavaScript Android开发
autojs模仿QQ长按弹窗菜单
我们自顶向下来写代码, 首先我们写的是setTestRecyclerViewAdapter.js, 他这个里面要做几件事: 加载两个类, Adapter和Holder, Holder先加载, 因为他会在Adapter中使用
253 0
仿QQ对话列表滑动删除与置顶的原理及实现(二)
仿QQ对话列表滑动删除与置顶的原理及实现(二)
109 0
仿QQ对话列表滑动删除与置顶的原理及实现(二)
|
Android开发 容器
仿QQ对话列表滑动删除与置顶的原理及实现(一)
仿QQ对话列表滑动删除与置顶的原理及实现(一)
153 0
仿QQ对话列表滑动删除与置顶的原理及实现(一)
|
开发者
QQ控件|学习笔记
快速学习QQ控件
QQ控件|学习笔记
|
开发工具 Android开发 容器
仿QQ聊天界面文字过长显示
前言 最近一直在做聊天功能,有群聊,有单聊,没有集成第三方SDK(例如环信)。从收到消息推送、插入数据库、到界面显示全是我们自己做的,在这个过程中碰到了很多问题,例如消息同步、前后台切换、界面刷新频率、收到上报等很多细节问题。
824 0