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提高二維碼識別率(一)-標記定位點
- autojs靈動球, 螢幕碎了
- autojs導航欄新增背景動畫
- autojs隨時翻譯剪貼簿單詞
- autojs懸浮窗模擬toast氣泡, 放到螢幕底部居中
- autojs識別數字-實戰
- autojs繪製玩家位置
- autojs-UI底部新增BottomAppBar
- autojs點贊按鈕動畫
- autojs色卡和拾色器
- autojs裁剪找圖
- autojs象棋識別棋子
- autojs操作列表-下載指令碼商店程式碼
- autojs資料驅動介面和介面驅動資料
- autojs對紅線藍線之間的部分進行顏色填充
- opencv-kmeans-圖片顏色量化
- autojs段子影片化
- UI介面的圖片需要主動回收
- 半自動開啟adb無線除錯