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

作者: 十个雨点 | 来源:发表于2017-01-16 16:47 被阅读4051次

转载注明出处:简书-十个雨点

通过辅助模式获取点击的文字的最后讲到的不足之处,促使我去实现更多的取词方式,复制方式选词显然是最直观最简单的方式。
但是咱们在用手机的时候经常会碰到这么一种情况,就是想复制某个应用内的某段文字却无法使用安卓默认的长按功能进行操作,为此,我参考全局复制这个应用,也实现了全局复制的功能,看似这是一个挺神奇、挺复杂的功能,其实只是对系统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<CopyNode>传递给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中还包含了监听系统按键功能和监听点击的文字的功能,阅读的时候不要被干扰了,感兴趣的可以看——通过辅助模式获取点击的文字使用辅助服务监听系统按键这两篇文章

相关文章

  • 使用Xposed框架实现全局复制

    转载注明出处:简书-十个雨点 简介 在使用辅助服务实现全局复制中,我介绍了通过辅助服务实现全局复制的功能,极大的提...

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

    转载注明出处:简书-十个雨点 通过辅助模式获取点击的文字的最后讲到的不足之处,促使我去实现更多的取词方式,复制方式...

  • redis 主从复制过程

    主从复制有两种状态,全局复制, 局部复制全局复制, 全局复制发生在从服务器启动时触发 局部复制很简单, 主服务器...

  • MySQL主从复制

    一.主从复制简介 为什么使用主从复制?1)高可用2)辅助备份3)分担负载 复制是 MySQL 的一项功能,允许服务...

  • 键盘输入辅助类KeyboardHook

    实现效果 本辅助类主要是用来方便实现全局键盘钩子,用来捕捉系统全局的键盘输入。 通过键盘钩子,我们可以获取用户的各...

  • Redis--复制

    Redis--复制 复制功能的实现 1.通过SLAVEOF命令可以让从服务器同步主服务器的数据。 旧版复制实现的机...

  • 改进版Snowflake全局ID生成器-uid-generato

    本文主要介绍 uid-generator (一种全局ID服务实现) uid-generator介绍 全局ID服务...

  • laravel中自定义辅助函数

    1 辅助函数 Laravel 包含各种各样的全局 PHP 「辅助」函数,框架本身也大量的使用了这些功能函数;如果你...

  • 移动端div点击有阴影效果

    可能会使用户不能使用长按复制,不建议全局使用。 div,a,img { -webkit-tap-highlight...

  • Redis复制

    1.概述 Redis使用复制功能在主从服务器之间实现数据的同步。Redis的复制功能分为两个版本: 2.8版本之前...

网友评论

    本文标题:使用辅助服务实现全局复制

    本文链接:https://www.haomeiwen.com/subject/keyhmttx.html