牙叔教程 简单易懂

效果预览

如上图所示, 一共有三个悬浮存储:

  • 右侧的悬浮球

  • 十字架悬浮窗

  • 翻译的内容悬浮窗

建议只用两个悬浮窗, 也就是把 十字架和翻译内容合并为一个悬浮窗, 这样用户在触摸移动的时候,

有更大的触摸区域.

思路

  1. 十字架指针确定单词位置

  2. 用autojs9提供的插件 MLKitOCR , 识别单词周围的内容

  3. ocr识别的结果中, 带有文字的recct信息,

  4. 这些rect信息和十字架的位置对比一下, 就知道是哪个单词了

  5. 网上找了个离线的词典, 词汇量几十万

  6. 用sqlite查询, 速度很快

离线词典制作教程

请参考: autojs查找相同词根的单词

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

这个是把csv的单词数据转成了db,

本教程不提供单词数据库, 若有需要, 下载那个开源的单词库, 跟着教程做, 自己转db

多个分辨率保持大致相似的布局

我们来看看多个分辨率的布局是否一样

这是模拟器的三种分辨率, 可以看到布局几乎一模一样, 这是怎么做的呢?

布局的宽高使用的都是px, 按照同样的比例来决定多宽多高,

文字大小使用的也是px; 他们使用的是同一个比例;

let config = {    cross: {        widthRatio: 0.05,    },    text: {        sizeRatio: 0.04,    },    translationHoveringWindow: {        widthRatio: 0.4,        heightRatio: 0.2,    },};

因此, 你看到的是同一个比例的布局, 但是是不同的宽高, 达到了在视觉上保持一致的效果

悬浮球

为了尽量少的遮挡用户的屏幕, 我们将其透明度设置为了0.5

alpha='0.5'

悬浮球随意拖动

他可以随意拖动

这个拖动因为比较常用, 建议大家封装一下;

我的代码已经写完了, 就不改了, 这里可以提供一个封装例子

首先写一个悬浮窗用来测试

let window = floaty.rawWindow(    <vertical>        <button id="move" text="移动" w="auto" h="auto" />    </vertical>);setInterval(() => {}, 1000);

然后是封装;

封装不只是移动, 后期可能还要添加各种按下拖动抬起的行为, 因此封装的参数对象有多个属性

let windowData = {    window: window,    moveViewId: "move",    isTouching: false,    downCallback: function () {},    moveCallback: function () {},    upCallback: function () {},    onClick: function () {},};

但是你肯定不想写这么多属性, 只有前两个是必须的, 其他的可有可无, 我们可以设置默认属性, 然后和用户传进来的对象合并

let windowData = {    window: window,    moveViewId: "move",};let windowDefaultData = {    isTouching: false,    downCallback: function () {},    moveCallback: function () {},    upCallback: function () {},    onClick: function () {},};Object.assign(windowDefaultData, windowData);

我们要让用户传入的参数对象覆盖掉默认的对象, 因此, 用户的参数作为 Object.assign 第二个参数

接下来是封装触摸移动;

第一: 做参数校验, 如果传递的参数对象没有必备的属性, 我们就抛出错误

function makeWindowMoveable(windowData) {    if (!windowData.window) {        throw new Error("windowData.window is undefined");    }    if (!windowData.moveViewId) {        throw new Error("windowData.moveViewId is undefined");    }

触摸移动的基础封装

var x = 0,y = 0;//记录按键被按下时的悬浮窗位置var windowX, windowY;//记录按键被按下的时间以便判断长按等动作var downTime;let view = window[windowData.moveViewId];view.setOnTouchListener(function (view, event) {switch (event.getAction()) {    case event.ACTION_DOWN:        x = event.getRawX();        y = event.getRawY();        windowX = window.getX();        windowY = window.getY();        return true;    case event.ACTION_MOVE:        //移动手指时调整悬浮窗位置        window.setPosition(windowX + (event.getRawX() - x), windowY + (event.getRawY() - y));        return true;    case event.ACTION_UP:        return true;}return true;});

在该基础上添加各种callback

switch (event.getAction()) {    case event.ACTION_DOWN:        windowDefaultData.downCallback();        return true;    case event.ACTION_MOVE:        windowDefaultData.moveCallback();        return true;    case event.ACTION_UP:        windowDefaultData.upCallback();        return true;}

再加上点击和长按操作

switch (event.getAction()) {    case event.ACTION_UP:        let upTime = new Date().getTime();        let time = upTime - downTime;        // 移动距离        let distance = Math.sqrt(Math.pow(event.getRawX() - x, 2) + Math.pow(event.getRawY() - y, 2));        if (time < 150 && distance < 5) {            windowDefaultData.onClick();        } else if (time > 2000 && distance < 5) {            windowDefaultData.longClick();        }        return true;}

完整封装如下

let window = floaty.rawWindow(    <vertical>        <button id="move" text="移动" w="auto" h="auto" />    </vertical>);setInterval(() => {}, 1000);let windowData = {    window: window,    moveViewId: "move",    downCallback: function () {        log("downCallback");    },    moveCallback: function () {        log("moveCallback");    },    upCallback: function () {        log("upCallback");    },    onClick: function () {        log("onClick");    },    longClick: function () {        log("longClick");    },};makeWindowMoveable(windowData);function makeWindowMoveable(windowData) {    if (!windowData.window) {        throw new Error("windowData.window is undefined");    }    if (!windowData.moveViewId) {        throw new Error("windowData.moveViewId is undefined");    }    let windowDefaultData = {        isTouching: false,        downCallback: function () {},        moveCallback: function () {},        upCallback: function () {},        onClick: function () {},        longClick: function () {},    };    Object.assign(windowDefaultData, windowData);    //记录按键被按下时的触摸坐标    var x = 0,        y = 0;    //记录按键被按下时的悬浮窗位置    var windowX, windowY;    //记录按键被按下的时间以便判断长按等动作    var downTime;    let view = window[windowData.moveViewId];    view.setOnTouchListener(function (view, event) {        switch (event.getAction()) {            case event.ACTION_DOWN:                windowDefaultData.isTouching = true;                x = event.getRawX();                y = event.getRawY();                windowX = window.getX();                windowY = window.getY();                downTime = new Date().getTime();                windowDefaultData.downCallback();                return true;            case event.ACTION_MOVE:                //移动手指时调整悬浮窗位置                window.setPosition(windowX + (event.getRawX() - x), windowY + (event.getRawY() - y));                windowDefaultData.moveCallback();                return true;            case event.ACTION_UP:                windowDefaultData.isTouching = false;                windowDefaultData.upCallback();                let upTime = new Date().getTime();                let time = upTime - downTime;                // 移动距离                let distance = Math.sqrt(Math.pow(event.getRawX() - x, 2) + Math.pow(event.getRawY() - y, 2));                if (time < 150 && distance < 5) {                    windowDefaultData.onClick();                } else if (time > 2000 && distance < 5) {                    windowDefaultData.longClick();                }                return true;        }        return true;    });    return windowDefaultData;}

这个封装能满足一部分使用需求, 在不同的使用场景仍要按需修改

悬浮窗吸附屏幕边缘

拖动以后会自动吸附到屏幕边缘;

靠近左边就贴到左边;

靠近右边就贴到右边;

位置会记住, 下次重启代码, 悬浮窗还在上次的位置

上面那个触摸还封装了一个是否正在触摸的属性, 为什么要加这个属性呢?

就是为了这个悬浮窗屏幕吸附效果;

吸附屏幕边缘, 我是用的是定时器, 每隔2秒, 检查一次

在我们移动悬浮窗的时候, 我们正在调整悬浮窗位置, 这个时候, 你肯定不想吸附到屏幕边缘;

不然, 他会干扰你调整位置

为了获取悬浮窗的状态, 这个封装方法 makeWindowMoveable 必须返回 windowDefaultData;

return windowDefaultData;

然后在你需要使用触摸状态的地方, 来获取触摸状态: windowDefaultData.isTouching

屏幕吸附边缘, 同样会记忆, 使用storage持久化存储

悬浮窗的实例化

悬浮窗还有各种隐藏, 显示, 以及一些其他方法要写, 因此建议封装为一个类, 这样我们可以更方便的调用各种方法, 而不用到处翻页找方法在哪里,

如果不这样做, 那就会越写越烦躁,一会找这个方法, 一会找那个方法, 烦死了

function CrossWindow() {    this.window = floaty.rawWindow(<vertical>...</vertical>);    this.isShowing = false;    this.storage = storages.create("crossWindow");}CrossWindow.prototype.getCrossPoint = function () {...};CrossWindow.prototype.toggle = function () {...};CrossWindow.prototype.hide = function () {...};......

悬浮窗显示和隐藏

隐藏是将悬浮窗移动到屏幕之外来实现的

this.window.setPosition(-10000, -10000);

显示就是移动到屏幕之内;

同样的, 悬浮窗的位置也会记忆, 也是用storage持久化存储

十字架

十字架是一个 frame 布局, 横着一个view, 竖着一个view, 就成了一个十字架

<frame id="cross">	<View layout_gravity="center" w="match_parent">	</View>	<View layout_gravity="center" h="match_parent">	</View></frame>

当然了, 你也可以用图片, 或者canvas自己画

十字架的中心点在屏幕上的位置

获取一个view在屏幕上的位置信息, 我们封装了一个方法

function getViewRect(view) {    let locationOnScreen = view.getLocationOnScreen();    let frame = new android.graphics.Rect();    view.getHitRect(frame);    let width = frame.width();    let height = frame.height();    return {        x: locationOnScreen[0],        y: locationOnScreen[1],        width: width,        height: height,    };}

获取十字架中心点, 就在获取位置信息后, 加减乘除, 很简单就不说了

点击翻译按钮, 识别单词

点击翻译按钮后, 我们先获取十字架的中心点, 然后调用autojs9

提供的插件 MLKitOCR , 这个是谷歌开发的工具, 识别英文效果又快又好,

ocr的识别结果带有单词的rect矩形坐标数据, 我们和十字架中心点对比,

谁包含十字架, 谁就是十字架指向的单词

封装的方法是判断一个点, 是否在一个rect中

function isPointInRect(point, rect) {    let x = point.x;    let y = point.y;    let x1 = rect.left;    let y1 = rect.top;    let x2 = rect.right;    let y2 = rect.bottom;    if (x >= x1 && x <= x2 && y >= y1 && y <= y2) {        return true;    }    return false;}

翻译单词

单词库上面已经说过了, 我们使用安卓的sqlite来查询单词, 命令是

db.rawQuery("SELECT * FROM " + TableName + " WHERE word = ?", [word]).single();

在识别过程中, ocr可能会带上句号, 问号, 分号之类的标点符号, 因此, 我们需要替换一下word

word = word.replace(/[:|,|\.]/g, "");

截图

截图的时候, 我们需要把十字架隐藏, 我用的方法是把十字架变透明

crossWindow.window.cross.attr("alpha", 0);sleep(60);ui.post(function () {    crossWindow.window.cross.attr("alpha", 1);}, 100);

不隐藏十字架的会, 会被截图, 影响识别单词的效果

ocr的识别区域

我们这里只讨论正常的英文文章,

我们假定的是一行最少3个单词, 一页最少20行英文单词;

let wordRect = {    ratio: {        w: 1 / 3,        h: 1 / 20,    },};

同样, 使用比例来决定图片的宽高

环境

设备: 小米11pro
Android版本: 12
雷电模拟器:9.0.17
Android版本: 9
Autojs版本: 9.2.13

名人名言

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

声明

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

举报/反馈

牙叔教程

110获赞 249粉丝
即学即用, 即用即学, 一切都是数据和结构
关注
0
0
收藏
分享