牙叔教程 简略易懂
上一节讲了列表和长按事情
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文档, 最终才是群里问问 — 牙叔教程
声明
部分内容来自网络 本教程仅用于学习, 禁止用于其他用途