使用輔助服務(wù)實(shí)現(xiàn)全局復(fù)制

轉(zhuǎn)載注明出處:簡書-十個(gè)雨點(diǎn)

通過輔助模式獲取點(diǎn)擊的文字的最后講到的不足之處僚楞,促使我去實(shí)現(xiàn)更多的取詞方式岸裙,復(fù)制方式選詞顯然是最直觀最簡單的方式建芙。
但是咱們?cè)谟檬謾C(jī)的時(shí)候經(jīng)常會(huì)碰到這么一種情況,就是想復(fù)制某個(gè)應(yīng)用內(nèi)的某段文字卻無法使用安卓默認(rèn)的長按功能進(jìn)行操作砚作,為此窘奏,我參考全局復(fù)制這個(gè)應(yīng)用,也實(shí)現(xiàn)了全局復(fù)制的功能葫录,看似這是一個(gè)挺神奇着裹、挺復(fù)雜的功能,其實(shí)只是對(duì)系統(tǒng)API的靈活調(diào)用米同。下面我就介紹一下骇扇,如何使用輔助服務(wù)實(shí)現(xiàn)全局復(fù)制摔竿。

先看看效果

全局復(fù)制觸發(fā)
全局復(fù)制觸發(fā)

也可以下載全能分詞體驗(yàn)

1. 如何使用輔助服務(wù)

這部分和通過輔助模式獲取點(diǎn)擊的文字基本一樣,但是需要注意的是xml中canRetrieveWindowContent必須設(shè)置成true少孝,否則無法獲取窗口內(nèi)容继低,自然也無法獲得文字?jǐn)?shù)據(jù)。

2. 如何獲取當(dāng)前頁面中文字以及位置

全局復(fù)制使用到了的系統(tǒng)API都是日常開發(fā)中不常用到的方法稍走。
先介紹幾個(gè)相關(guān)方法:

AccessibilityService的getRootInActiveWindow方法:
public AccessibilityNodeInfo getRootInActiveWindow()
用于獲取當(dāng)前窗口的根對(duì)象袁翁,其中AccessibilityNodeInfo是用來在輔助服務(wù)中表示的View的對(duì)象,包含文字婿脸、位置粱胜、子View等信息。

AccessibilityNodeInfo的getChild方法:
public AccessibilityNodeInfo getChild(int index) 
用于獲取當(dāng)前對(duì)象的子View的對(duì)應(yīng)對(duì)象

AccessibilityNodeInfo的getBoundsInScreen方法:
public Rect getBoundsInScreen() 
用于獲取當(dāng)前對(duì)象代表的View在屏幕中的位置狐树,返回值是一個(gè)Rect對(duì)象

AccessibilityNodeInfo的getText()方法:
用于獲取當(dāng)前對(duì)象代表的View中的文本

AccessibilityNodeInfo的getContentDescription方法:
用于獲取當(dāng)前對(duì)象代表的View中的內(nèi)容的描述焙压,在有些View中可以作為getText方法的補(bǔ)充

知道了這些方法的功能,要獲得當(dāng)前頁面中的文字及其位置就很簡單了抑钟,直接看代碼:
首先涯曲,我們?cè)O(shè)計(jì)一種數(shù)據(jù)結(jié)構(gòu),用于記錄文字和位置

public class CopyNode implements Parcelable {
    public static Creator<CopyNode> CREATOR = new Creator<CopyNode>() {

        @Override
        public CopyNode createFromParcel(Parcel source) {
            return new CopyNode(source);
        }

        @Override
        public CopyNode[] newArray(int size) {
            return new CopyNode[size];
        }
    };

    private Rect bound;
    private String content;

    public CopyNode(Rect var1, String var2) {
        this.bound = var1;
        this.content = var2;
    }

    public CopyNode(Parcel var1) {
        this.bound = new Rect(var1.readInt(), var1.readInt(), var1.readInt(), var1.readInt());
        this.content = var1.readString();
    }

    public long caculateSize() {
        return (long)(this.bound.width() * this.bound.height());
    }

    public Rect getBound() {
        return this.bound;
    }

    public String getContent() {
        return this.content;
    }

    public int describeContents() {
        return 0;
    }

    public void writeToParcel(Parcel var1, int var2) {
        var1.writeInt(this.bound.left);
        var1.writeInt(this.bound.top);
        var1.writeInt(this.bound.right);
        var1.writeInt(this.bound.bottom);
        var1.writeString(this.content);
    }

    @Override
    public String toString() {
        return "CopyNode{" +
                "bound=" + bound +
                ", content='" + content + '\'' +
                '}';
    }
}

然后再看如何獲取數(shù)據(jù)


private int retryTimes = 0;

@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
private void UniversalCopy() {
    boolean isSuccess=false;
    labelOut: {
        AccessibilityNodeInfo rootInActiveWindow = this.getRootInActiveWindow();
        if(retryTimes < 10) {
            String packageName;
            if(rootInActiveWindow != null) {
                packageName = String.valueOf(rootInActiveWindow.getPackageName());
            } else {
                packageName = null;
            }

            if(rootInActiveWindow == null || packageName != null && packageName.contains("com.android.systemui")) {
                //如果通知欄沒有收起來味赃,則延遲進(jìn)行
                ++retryTimes;
                handler.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        UniversalCopy();
                    }
                }, 100);
                return;
            }

            //獲取屏幕高寬掀抹,用于遍歷數(shù)據(jù)時(shí)確定邊界虐拓。
            WindowManager windowManager = (WindowManager)this.getSystemService(Context.WINDOW_SERVICE);
            DisplayMetrics displayMetrics = new DisplayMetrics();
            windowManager.getDefaultDisplay().getMetrics(displayMetrics);
            int heightPixels = displayMetrics.heightPixels;
            int widthPixels = displayMetrics.widthPixels;

            ArrayList nodeList = traverseNode(new AccessibilityNodeInfoCompat(rootInActiveWindow), widthPixels, heightPixels);
            if(nodeList.size() > 0) {
                Intent intent = new Intent(this, CopyActivity.class);
                intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                intent.putParcelableArrayListExtra("copy_nodes", nodeList);
                intent.putExtra("source_package", packageName);
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
                    this.startActivity(intent, ActivityOptions.makeCustomAnimation(this.getBaseContext(), android.R.anim.fade_in, android.R.anim.fade_out).toBundle());
                }else {
                    startActivity(intent);
                }
                isSuccess = true;
                break labelOut;
            }
        }

        isSuccess = false;
    }

    if(!isSuccess) {
        if (!BigBangMonitorService.isAccessibilitySettingsOn(this)){
            ToastUtil.show(R.string.error_in_permission);
        }else {
            ToastUtil.show(R.string.error_in_copy);
        }

    }

    retryTimes = 0;
}

private ArrayList<CopyNode> traverseNode(AccessibilityNodeInfoCompat nodeInfo, int width, int height) {
    ArrayList<CopyNode> nodeList = new ArrayList();
    if(nodeInfo != null && nodeInfo.getInfo() != null) {
        nodeInfo.refresh();

        for(int i = 0; i < nodeInfo.getChildCount(); ++i) {
            //遞歸遍歷nodeInfo
            nodeList.addAll(traverseNode(nodeInfo.getChild(i), width, height));
        }

        if(nodeInfo.getClassName() != null && nodeInfo.getClassName().equals("android.webkit.WebView")) {
            return nodeList;
        } else {
            String content = null;
            String description = content;
            if(nodeInfo.getContentDescription() != null) {
                description = content;
                if(!"".equals(nodeInfo.getContentDescription())) {
                    description = nodeInfo.getContentDescription().toString();
                }
            }

            content = description;
            if(nodeInfo.getText() != null) {
                content = description;
                if(!"".equals(nodeInfo.getText())) {
                    content = nodeInfo.getText().toString();
                }
            }

            if(content != null) {
                Rect outBounds = new Rect();
                nodeInfo.getBoundsInScreen(outBounds);
                if(checkBound(outBounds, width, height)) {
                    nodeList.add(new CopyNode(outBounds, content));
                }
            }

            return nodeList;
        }
    } else {
        return nodeList;
    }
}


private boolean checkBound(Rect var1, int var2, int var3) {
    //檢測(cè)邊界是否符合規(guī)范
    return var1.bottom >= 0 && var1.right >= 0 && var1.top <= var3 && var1.left <= var2;
}

代碼不難心俗,就是通過遞歸的方式,獲取所有在屏幕范圍內(nèi)的文字及其位置蓉驹。

3. 讓用戶選擇要復(fù)制的文字

獲取當(dāng)前窗口中的文字及其位置是在Service中完成的城榛,而讓用戶進(jìn)行選擇,則必須切換到Activity中進(jìn)行展示和交互态兴。在UniversalCopy()方法的最后狠持,已經(jīng)將獲得的ArrayList<CopyNode>傳遞給Activity了,在Activity中取出數(shù)據(jù)并添加到顯示界面中:

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ...
    Bundle extras = getIntent().getExtras();
    if (extras==null){
        finish();
        return;
    }
    extras.setClassLoader(CopyNode.class.getClassLoader());

    String packageName = extras.getString("source_package");
    height = statusBarHeight;

    ArrayList nodesList = extras.getParcelableArrayList("copy_nodes");
    if(nodesList != null && nodesList.size() > 0) {
        CopyNode[] nodes = (CopyNode[])nodesList.toArray(new CopyNode[0]);
        Arrays.sort(nodes, new CopyNodeComparator());
        for(int i  = 0; i < nodes.length; ++i) {
            (new CopyNodeView(this, nodes[i])).addToFrameLayout(copyNodeViewContainer, height);
        }
    } else {
        ToastUtil.show(R.string.error_in_copy);
        finish();
    }
    ...
}
public class CopyNodeComparator implements Comparator<CopyNode> {
    //按面積從大到小排序
    public int compare(CopyNode o1, CopyNode o2) {
        long o1Size = o1.caculateSize();
        long o2Size = o2.caculateSize();
        return o1Size < o2Size?-1:(o1Size == o2Size?0:1);
    }
}

為什么CopyNodeComparator 要按照從大到小的順序進(jìn)行排列呢瞻润,因?yàn)槿绻娣e大的View放在下面喘垂,就會(huì)把小的View遮蓋住,小View就無法被點(diǎn)擊到了绍撞。
其中CopyNodeView是用來展示文本的位置View:


public class CopyNodeView extends View {
    private Rect bound;
    private String content;
    private boolean selected = false;

    ...
    public CopyNodeView(Context context, CopyNode copyNode) {
        super(context);
        this.bound = copyNode.getBound();
        this.content = copyNode.getContent();
    }

    public void addToFrameLayout(FrameLayout frameLayout, int height) {
        LayoutParams var3 = new LayoutParams(this.bound.width(), this.bound.height());
        var3.leftMargin = this.bound.left;
        var3.topMargin = Math.max(0, this.bound.top - height);
        var3.width = this.bound.width();
        var3.height = this.bound.height();
        frameLayout.addView(this, 0, var3);
    }
    ...
}

除了這些核心代碼以外正勒,再設(shè)置好CopyNodeView的點(diǎn)擊事件、菜單項(xiàng)的響應(yīng)等其他雜七雜八的工作以后傻铣,全局復(fù)制功能就完成了章贞。

源碼

完整代碼可以參考Bigbang項(xiàng)目的BigBangMonitorService、CopyActivity非洲、CopyNode鸭限、CopyNodeView等類蜕径。

ps:BigBangMonitorService中還包含了監(jiān)聽系統(tǒng)按鍵功能和監(jiān)聽點(diǎn)擊的文字的功能,閱讀的時(shí)候不要被干擾了败京,感興趣的可以看——通過輔助模式獲取點(diǎn)擊的文字使用輔助服務(wù)監(jiān)聽系統(tǒng)按鍵這兩篇文章

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末兜喻,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子喧枷,更是在濱河造成了極大的恐慌虹统,老刑警劉巖,帶你破解...
    沈念sama閱讀 210,914評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件隧甚,死亡現(xiàn)場(chǎng)離奇詭異车荔,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)戚扳,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評(píng)論 2 383
  • 文/潘曉璐 我一進(jìn)店門忧便,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人帽借,你說我怎么就攤上這事珠增。” “怎么了砍艾?”我有些...
    開封第一講書人閱讀 156,531評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵蒂教,是天一觀的道長。 經(jīng)常有香客問我脆荷,道長凝垛,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,309評(píng)論 1 282
  • 正文 為了忘掉前任蜓谋,我火速辦了婚禮梦皮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘桃焕。我一直安慰自己剑肯,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,381評(píng)論 5 384
  • 文/花漫 我一把揭開白布观堂。 她就那樣靜靜地躺著让网,像睡著了一般。 火紅的嫁衣襯著肌膚如雪师痕。 梳的紋絲不亂的頭發(fā)上溃睹,一...
    開封第一講書人閱讀 49,730評(píng)論 1 289
  • 那天,我揣著相機(jī)與錄音七兜,去河邊找鬼丸凭。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的惜犀。 我是一名探鬼主播铛碑,決...
    沈念sama閱讀 38,882評(píng)論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼虽界!你這毒婦竟也來了汽烦?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,643評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤莉御,失蹤者是張志新(化名)和其女友劉穎撇吞,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體礁叔,經(jīng)...
    沈念sama閱讀 44,095評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡牍颈,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,448評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了琅关。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片煮岁。...
    茶點(diǎn)故事閱讀 38,566評(píng)論 1 339
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖涣易,靈堂內(nèi)的尸體忽然破棺而出画机,到底是詐尸還是另有隱情,我是刑警寧澤新症,帶...
    沈念sama閱讀 34,253評(píng)論 4 328
  • 正文 年R本政府宣布步氏,位于F島的核電站,受9級(jí)特大地震影響徒爹,放射性物質(zhì)發(fā)生泄漏荚醒。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,829評(píng)論 3 312
  • 文/蒙蒙 一瀑焦、第九天 我趴在偏房一處隱蔽的房頂上張望腌且。 院中可真熱鬧梗肝,春花似錦榛瓮、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至坝锰,卻和暖如春粹懒,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背顷级。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評(píng)論 1 264
  • 我被黑心中介騙來泰國打工凫乖, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,248評(píng)論 2 360
  • 正文 我出身青樓帽芽,卻偏偏與公主長得像删掀,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子导街,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,440評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,727評(píng)論 25 707
  • ¥開啟¥ 【iAPP實(shí)現(xiàn)進(jìn)入界面執(zhí)行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開一個(gè)線程披泪,因...
    小菜c閱讀 6,362評(píng)論 0 17
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件搬瑰、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 12,058評(píng)論 4 62
  • static 這個(gè)關(guān)鍵字意義很多款票。c語言中static 函數(shù)和普通函數(shù)的區(qū)別? so庫接口 Linux 共享庫中函...
    丁不想被任何狗咬閱讀 321評(píng)論 0 0
  • 文/三萬夜 買了一件麻質(zhì)地連衣裙 忽然來訪的閨蜜說泽论。 我剛好看完一本書艾少。 兩人走向郊外 姍姍的走向夏天。
    三萬夜閱讀 175評(píng)論 0 1