01月16, 2017

使用辅助服务实现全局复制

通过辅助模式获取点击的文字的最后讲到的不足之处,促使我去实现更多的取词方式,复制方式选词显然是最直观最简单的方式。

但是咱们在用手机的时候经常会碰到这么一种情况,就是想复制某个应用内的某段文字却无法使用安卓默认的长按功能进行操作,为此,我参考全局复制这个应用,也实现了全局复制的功能,看似这是一个挺神奇、挺复杂的功能,其实只是对系统API的灵活调用。下面我就介绍一下,如何使用辅助服务实现全局复制。

先看看效果

全局复制触发

也可以下载全能分词体验

1. 如何使用辅助服务

这部分和通过辅助模式获取点击的文字基本一样,但是需要注意的是xml中canRetrieveWindowContent必须设置成true,否则无法获取窗口内容,自然也无法获得文字数据。

2. 如何获取当前页面中文字以及位置

全局复制使用到了的系统API都是日常开发中不常用到的方法。 先介绍几个相关方法:

AccessibilityService的getRootInActiveWindow方法:
public AccessibilityNodeInfo getRootInActiveWindow()
用于获取当前窗口的根对象,其中AccessibilityNodeInfo是用来在辅助服务中表示的View的对象,包含文字、位置、子View等信息。

AccessibilityNodeInfo的getChild方法:
public AccessibilityNodeInfo getChild(int index) 
用于获取当前对象的子View的对应对象

AccessibilityNodeInfo的getBoundsInScreen方法:
public Rect getBoundsInScreen() 
用于获取当前对象代表的View在屏幕中的位置,返回值是一个Rect对象

AccessibilityNodeInfo的getText()方法:
用于获取当前对象代表的View中的文本

AccessibilityNodeInfo的getContentDescription方法:
用于获取当前对象代表的View中的内容的描述,在有些View中可以作为getText方法的补充

知道了这些方法的功能,要获得当前页面中的文字及其位置就很简单了,直接看代码: 首先,我们设计一种数据结构,用于记录文字和位置

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 + '\'' +
                '}';
    }
}

然后再看如何获取数据


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")) {
                //如果通知栏没有收起来,则延迟进行
                ++retryTimes;
                handler.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        UniversalCopy();
                    }
                }, 100);
                return;
            }

            //获取屏幕高宽,用于遍历数据时确定边界。
            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) {
    //检测边界是否符合规范
    return var1.bottom >= 0 && var1.right >= 0 && var1.top <= var3 && var1.left <= var2;
}

代码不难,就是通过递归的方式,获取所有在屏幕范围内的文字及其位置。

3. 让用户选择要复制的文字

获取当前窗口中的文字及其位置是在Service中完成的,而让用户进行选择,则必须切换到Activity中进行展示和交互。在UniversalCopy()方法的最后,已经将获得的ArrayList传递给Activity了,在Activity中取出数据并添加到显示界面中:

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 要按照从大到小的顺序进行排列呢,因为如果面积大的View放在下面,就会把小的View遮盖住,小View就无法被点击到了。 其中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);
    }
    ...
}

除了这些核心代码以外,再设置好CopyNodeView的点击事件、菜单项的响应等其他杂七杂八的工作以后,全局复制功能就完成了。

源码

完整代码可以参考Bigbang项目的BigBangMonitorService、CopyActivity、CopyNode、CopyNodeView等类。

ps:BigBangMonitorService中还包含了监听系统按键功能和监听点击的文字的功能,阅读的时候不要被干扰了,感兴趣的可以看——通过辅助模式获取点击的文字使用辅助服务监听系统按键这两篇文章

转载注明出处:十个雨点

本文链接:http://www.siki.space/post/universal_copy_through_accessibility_service.html

-- EOF --

Comments