Android后臺(tái)模擬點(diǎn)擊探索(附源碼)

工作中我們需要自制一套工具,其中遇到需要模擬點(diǎn)擊事件的需求,類似按鍵精靈的功能,支持后臺(tái)持續(xù)運(yùn)行,滿足觸發(fā)條件時(shí)完成點(diǎn)擊。

創(chuàng)新互聯(lián)專注于企業(yè)成都全網(wǎng)營(yíng)銷、網(wǎng)站重做改版、劍川網(wǎng)站定制設(shè)計(jì)、自適應(yīng)品牌網(wǎng)站建設(shè)、H5技術(shù)、商城網(wǎng)站建設(shè)、集團(tuán)公司官網(wǎng)建設(shè)、成都外貿(mào)網(wǎng)站建設(shè)公司、高端網(wǎng)站制作、響應(yīng)式網(wǎng)頁(yè)設(shè)計(jì)等建站業(yè)務(wù),價(jià)格優(yōu)惠性價(jià)比高,為劍川等各大城市提供網(wǎng)站開(kāi)發(fā)制作服務(wù)。

經(jīng)過(guò)一番探索,一共整理出兩種不同的方案:AccessibilityService 和 adb shell命令,讀者可自行選擇合適的場(chǎng)景。

AccessibilityService

無(wú)障礙模式是我首先想到的方案,對(duì)于不知道Android無(wú)障礙模式的,可自行百度。這里簡(jiǎn)單說(shuō)明一下,AccessibilityService是Android為殘障人士提供的貼心功能,比如可以報(bào)出當(dāng)前頁(yè)面有哪些按鈕balabala。使用官方提供的一些列API,我們還可以完成一些自動(dòng)運(yùn)行的“黑科技”操作,比如早些年的紅包插件、微信自動(dòng)回復(fù)插件、自動(dòng)點(diǎn)贊插件等。

本方案原理比較簡(jiǎn)單:掃描當(dāng)前頁(yè)面的View樹(shù),找到目標(biāo)控件,模擬點(diǎn)擊操作,下面詳細(xì)闡述。

添加配置文件

首先需要在res目錄下建立配置文件:accessible_service_config.xml ,名字隨意取。

<?xml version="1.0" encoding="utf-8"?>
<accessibility-service
 xmlns:android="http://schemas.android.com/apk/res/android"
 android:accessibilityEventTypes="typeAllMask"
 android:accessibilityFeedbackType="feedbackGeneric"
 android:accessibilityFlags="flagReportViewIds"
 android:canRetrieveWindowContent="true"
 android:notificationTimeout="100"
 android:description="@string/description"
 android:packageNames="目標(biāo)包名"/>

accessibilityEventTypes:設(shè)置響應(yīng)事件的類型,這里設(shè)置typeAllMask,就是響應(yīng)全部類型的事件。

accessibilityFeedbackType:設(shè)置回饋給用戶的方式,有語(yǔ)音播出和振動(dòng),這里使用通用類型。

notificationTimeout:設(shè)置響應(yīng)時(shí)間。

packageNames:目標(biāo)包名,比如紅包插件就要設(shè)置微信包名,關(guān)于包名如何獲取,下文會(huì)提到。

繼承AccessibilityService編碼

接著我們繼承AccessibilityService新建AutoClickAccessibilityService,重寫onAccessibilityEvent(AccessibilityEvent event)。

public class AutoClickAccessibilityService extends AccessibilityService {
 private static final String TAG = "GK";

 @Override
 public void onAccessibilityEvent(AccessibilityEvent event) {
  ztLog("===start===");
  try {
   //拿到根節(jié)點(diǎn)
   AccessibilityNodeInfo rootInfo = getRootInActiveWindow();
   if (rootInfo == null) {
    return;
   //開(kāi)始遍歷,這里拎出來(lái)細(xì)講,直接往下看正文
   if (rootInfo.getChildCount() != 0) {
    ……
   }
  } catch (Exception e) {
  ztLog("Exception:" + e.getMessage(), true);
 }
}

拿到根節(jié)點(diǎn)以后,我們有兩種方式開(kāi)始尋找目標(biāo)節(jié)點(diǎn):

  1. 根據(jù)View id:findAccessibilityNodeInfosByViewId
  2. 根據(jù)控件文案:findAccessibilityNodeInfosByText

這里我們拿魅族手機(jī)自帶的音樂(lè)App做例子,假如我們需要自動(dòng)點(diǎn)擊下圖的 專欄 :

Android后臺(tái)模擬點(diǎn)擊探索(附源碼)

使用findAccessibilityNodeInfosByViewId尋找目標(biāo)

我們可以使用findAccessibilityNodeInfosByViewId(),通過(guò)id找到目標(biāo)節(jié)點(diǎn),關(guān)于View id,可以使用DDMS中的Dump View Hierarchy for UI Automator,就是點(diǎn)擊下圖按鈕(不知道如何打開(kāi)eclipse或者AS的DDMS的同學(xué)可以自行百度):

Android后臺(tái)模擬點(diǎn)擊探索(附源碼)

稍等片刻,生成屏幕快照,并解析出View樹(shù),從右下的屬性框就可以找到id,同時(shí)仔細(xì)看,包名也可以獲取到啦~

Android后臺(tái)模擬點(diǎn)擊探索(附源碼)

這里很有可能因?yàn)槟繕?biāo)apk混淆嚴(yán)重而讀不到id,比如是個(gè)?,那么可以嘗試第二個(gè)方法。

使用findAccessibilityNodeInfosByText尋找目標(biāo)

使用findAccessibilityNodeInfosByText("最熱MV"),顧名思義,就是根據(jù)文案找控件。

找到控件以后,就可以執(zhí)行點(diǎn)擊操作了,但是且慢,這里有個(gè)坑。

因?yàn)樽⒁饪催@里的view樹(shù):

Android后臺(tái)模擬點(diǎn)擊探索(附源碼)

無(wú)論我們根據(jù)id還是文案,找到的可能只是一個(gè)TextView或者Button,但是根據(jù)我們?nèi)粘=?jīng)驗(yàn),我們肯定是給其父布局設(shè)置的點(diǎn)擊事件,也就是這里的LinearLayout或者FrameLayout。

所以我的方案是根據(jù)View樹(shù)的結(jié)構(gòu),自行遍歷。比如這里的View樹(shù)結(jié)構(gòu)如下:

Android后臺(tái)模擬點(diǎn)擊探索(附源碼)

我先做深度優(yōu)先遍歷找到GridView,然后遍歷它所有孩子直至找到專欄這個(gè)TextView,為什么我不直接DFS找到專欄呢?因?yàn)槲乙涗浰母腹?jié)點(diǎn)甚至爺爺節(jié)點(diǎn),方便接下來(lái)執(zhí)行點(diǎn)擊操作。

如果有同學(xué)使用這種方案,建議根據(jù)實(shí)際View樹(shù)的結(jié)構(gòu),自行遍歷尋找,我的代碼如下:

/**
 * 深度優(yōu)先遍歷尋找目標(biāo)節(jié)點(diǎn)
 */
private void DFS(AccessibilityNodeInfo rootInfo) {
  if (rootInfo == null || TextUtils.isEmpty(rootInfo.getClassName())) {
    return;
  }
  if (!"android.widget.GridView".equals(rootInfo.getClassName())) {
    ztLog(rootInfo.getClassName().toString());
    for (int i = 0; i < rootInfo.getChildCount(); i++) {
      DFS(rootInfo.getChild(i));
    }
  } else {
    ztLog("==find gridView==");
    final AccessibilityNodeInfo GridViewInfo = rootInfo;
    for (int i = 0; i < GridViewInfo.getChildCount(); i++) {
      final AccessibilityNodeInfo frameLayoutInfo = GridViewInfo.getChild(i);
      //細(xì)心的同學(xué)會(huì)發(fā)現(xiàn),我代碼里的遍歷的邏輯跟View樹(shù)里顯示的結(jié)構(gòu)不一樣,
      //快照顯示的FrameLayout下明明該是LinearLayout,我這里卻是TextView,
      //這個(gè)我也不知道,實(shí)際調(diào)試出來(lái)的就是這樣……所以大家實(shí)操過(guò)程中也要注意了
      final AccessibilityNodeInfo childInfo = frameLayoutInfo.getChild(0);
      String text = childInfo.getText().toString();
      if (text.equals("專欄")) {
        performClick(frameLayoutInfo);
      } else {
        ztLog(text);
      }
    }
  }
}

private void performClick(AccessibilityNodeInfo targetInfo) {
  targetInfo.performAction(AccessibilityNodeInfo.ACTION_CLICK);
}

AndroidManifest文件添加Service配置

AccessibilityService也是一個(gè)Servcie,所以要在AndroidManifest配置一下。

<service
  android:name=".AutoClickService"
  android:exported="false"
  <!-- label就是在手機(jī)設(shè)置中的無(wú)障礙里,顯示的標(biāo)簽 -->
  android:label="自動(dòng)點(diǎn)擊Demo"
  <!-- 注意這里的android:permission是在service結(jié)構(gòu)里面的!! -->
  android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE" >
  <intent-filter>
    <action android:name="android.accessibilityservice.AccessibilityService" />
  </intent-filter>
  <!-- 配置服務(wù)服務(wù)配置文件路徑 -->
  <meta-data
    android:name="android.accessibilityservice"
    android:resource="@xml/accessible_service_config" />
</service>

 至此無(wú)障礙模式方案就講完了,運(yùn)行之后,需要在手機(jī)設(shè)置中的無(wú)障礙里打開(kāi)對(duì)應(yīng)的開(kāi)關(guān):

Android后臺(tái)模擬點(diǎn)擊探索(附源碼)

Android后臺(tái)模擬點(diǎn)擊探索(附源碼)

打開(kāi)以后,自動(dòng)點(diǎn)擊功能可以自動(dòng)后臺(tái)運(yùn)行了,不想用時(shí)可以在上圖開(kāi)關(guān)那里關(guān)閉即可。

以后需要先運(yùn)行App,再打開(kāi)開(kāi)關(guān),開(kāi)啟功能。

無(wú)障礙模式雖然用著挺舒服,但是在很多廠商的系統(tǒng)里,已經(jīng)打開(kāi)的無(wú)障礙模式隔一段時(shí)間經(jīng)常會(huì)被自動(dòng)關(guān)閉,比如MIUI系統(tǒng)里就要給App加開(kāi)機(jī)運(yùn)行的權(quán)限。

而廠商自帶的無(wú)障礙就沒(méi)事,猜測(cè)系統(tǒng)里內(nèi)置了處理,這也是無(wú)障礙模式的一個(gè)坑吧。

小結(jié)

最后總結(jié)一下,AccessibilityService是一個(gè)很有趣的功能,發(fā)揮想象力可以做很多事,但是要小心踩坑:

  1. 通過(guò)findAccessibilityNodeInfosByViewId或者findAccessibilityNodeInfosByText找到的目標(biāo)控件不一定是你想要的點(diǎn)擊控件
  2. 各家廠商系統(tǒng)可能對(duì)無(wú)障礙模式內(nèi)置了屏蔽處理

adb shell命令

adb可以方便我們直接高效的操作真機(jī),比如安裝apk,批量安裝apk,復(fù)制文件等,而模擬點(diǎn)擊事件也是可以通過(guò)adb命令完成的。

我是突然想到,前陣子看過(guò)網(wǎng)上流傳的一個(gè)“微信跳一跳”的輔助,使用python + adb完成。

原理就是adb負(fù)責(zé)截圖,python負(fù)責(zé)圖像識(shí)別像素計(jì)算距離,最后再由adb模擬點(diǎn)擊。

如果我們需要點(diǎn)擊的目標(biāo),坐標(biāo)相對(duì)確定,那我們直接在代碼里執(zhí)行adb命令模擬點(diǎn)擊即可。

真機(jī)實(shí)驗(yàn)

我們先用USB連接真機(jī),在cmd命令行工具里:

adb shell
shell@PRO6:/ $ input tap 125 521
shell@PRO6:/ $ 

這里的意思就是點(diǎn)擊屏幕上 (x, y) = (125, 521)的地方。果然手機(jī)響應(yīng)了,缺點(diǎn)就是響應(yīng)時(shí)間略長(zhǎng),感覺(jué)有1秒左右。

同理其他手勢(shì)操作也可以完成,這里不作詳解,感興趣的可以自行搜索。

下面我們需要做的就是在代碼里完成上述操作,并且可以持續(xù)在后臺(tái)運(yùn)行。這里我也是踩坑無(wú)數(shù),聽(tīng)我慢慢吐槽。

尋找后臺(tái)執(zhí)行adb命令的方案

ProcessBuilder — OUT

沒(méi)什么好說(shuō)的,直接看代碼:

  int x = 0, y = 0;
  String[] order = { "input", "tap", " ", x + "", y + "" };
  try {
    new ProcessBuilder(order).start();
  } catch (IOException e) {
    Log.i("GK", e.getMessage());
    e.printStackTrace();
  }

這種版本,在Activity中可行,但是切后臺(tái)不行……這肯定無(wú)法滿足需求,再找!

Instrumentation — OUT

try {
  Instrumentation inst = new Instrumentation();
  inst.sendPointerSync(MotionEvent.obtain(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN, x, y, 0));
  inst.sendPointerSync(MotionEvent.obtain(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, x, y, 0));
  Log.i("GK", "模擬點(diǎn)擊" + x + ", " + y);
} catch (Exception e) {
  Log.e("Exception when sendPointerSync", e.toString());
}

這種版本和上一個(gè)一模一樣,不能后臺(tái),差評(píng)??!

救世主Runtime登場(chǎng)

private OutputStream os;

/**
 * 執(zhí)行ADB命令: input tap 125 340
 */
private final void exec(String cmd) {
  try {
    if (os == null) {
      os = Runtime.getRuntime().exec("su").getOutputStream();
    }
    os.write(cmd.getBytes());
    os.flush();
  } catch (Exception e) {
    e.printStackTrace();
    Log.e("GK", e.getMessage());
  }
}

后臺(tái)問(wèn)題迎刃而解!

添加合適的時(shí)機(jī)

目前我們把核心功能做完了,最后需要做的就是找到合適的時(shí)機(jī),執(zhí)行操作。

首先我們的容器肯定是一個(gè)Service,然后后臺(tái)不斷的判斷當(dāng)前app是否是目標(biāo)app,如果是的話,再執(zhí)行自動(dòng)點(diǎn)擊操作。

所以我們需要判斷當(dāng)前前臺(tái)app的包名或者Activity的名字是否是我們的目標(biāo)。

/**
 * 如果前臺(tái)APP是目標(biāo)apk
 */
private boolean isCurrentAppIsTarget() {
  String name = getForegroundAppPackageName();
  if (!TextUtils.isEmpty(name) && PACKAGE_NAME.equalsIgnoreCase(name)) {
    return true;
  }
  return false;
}

/**
 * 獲取前臺(tái)程序包名
 */
public String getForegroundAppPackageName() {
  ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
  List<RunningAppProcessInfo> lr = am.getRunningAppProcesses();
  if (lr == null) {
    return null;
  }

  for (RunningAppProcessInfo ra : lr) {
    if (ra.importance == RunningAppProcessInfo.IMPORTANCE_VISIBLE || ra.importance == RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
      Log.i("GK", ra.processName);
      return ra.processName;
    }
  }
  return "";
}

以上就是adb shell方案,這種方案缺陷也比較明顯,就是要求 自動(dòng)點(diǎn)擊的位置不能改變。

對(duì)于如何獲取點(diǎn)擊位置的坐標(biāo),可以打開(kāi)開(kāi)發(fā)者選項(xiàng)中的指針位置:

Android后臺(tái)模擬點(diǎn)擊探索(附源碼)

直接查看坐標(biāo)。

總結(jié)

模擬點(diǎn)擊這種需求,我們一般都不會(huì)用到,也有點(diǎn)歪門邪道的意思。但是無(wú)論什么需求,中間的探索過(guò)程才最珍貴。技術(shù)也是人,不是每次都會(huì)有說(shuō)干就干的決心和勇氣,保持一顆好奇心,珍惜每次探索的機(jī)會(huì),學(xué)有所得,小有收獲,也未嘗不是一種自我認(rèn)可。

最后附上源碼:AutoClickService

以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持創(chuàng)新互聯(lián)。

本文名稱:Android后臺(tái)模擬點(diǎn)擊探索(附源碼)
分享URL:http://muchs.cn/article44/iiooee.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供網(wǎng)站導(dǎo)航、電子商務(wù)、手機(jī)網(wǎng)站建設(shè)網(wǎng)站設(shè)計(jì)、虛擬主機(jī)全網(wǎng)營(yíng)銷推廣

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)

外貿(mào)網(wǎng)站制作