轉(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ù)制摔竿。
先看看效果
也可以下載全能分詞體驗(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)按鍵這兩篇文章