APP发布之后,发布者最关心的事情自然是用户使用APP的情况。换言之,他们希望回答一个问题:我的用户是怎么使用我的APP的?为了回答这个问题,最重要的步骤就是用户数据收集,因为用户不可能来到你的身边去展示他对APP的使用方法。那么只能让数据来告诉你答案。
Queen的工作便是跟踪用户对APP的使用过程,其中,识别用户的操作是Queen的核心内容。用户对APP的动作包括点击,滑动等等。识别动作的思路自然可以是从控件上下功夫,因为用户的操作的直接对象便是控件。那么对于一个点击,自然可以通过以下方法去采集数据。
button.setOnClickListener(new View.OnClickListener(){
onClick(View v){
//插入数据采集接口
}
})
但是,一个APP可能有成百个控件,甚至一个页面就有好几个。统统这样插入接口工作量巨大,所以并不是一个最优的方法。Queen的采集过程来自Android点击信号传递方式。总所周知,在Android中最先感知点击事件的是rootView,之后通过 onInterceptTouchEvent()接口向子View传递该信息,直到消费该点击事件的view为止。之后onTouchEvent将该事件从子View向父View传递。
这个工作原理为我们抓取点击事件提供了线索,这里由于rootView包含了整个View上的所有控件,那么对于一个点击事件,我们只需要知道点击位置,然后依据点击位置,从rootView开始一层层去查最顶上的,可点击的时间就可以了。
详细代码请参考 https://github.com/xiaofeifei3/queen。
Queen的解决方案是这样子:
/**
* 识别在View上所进行的动作
* Get the operation on the view.
*
* @param ev 动作; Operation;
* @param myView 执行动作的View; View on the screen;
* @param context
*/
public void recognizeViewEvent(MotionEvent ev, View myView, Context context){
switch(ev.getAction()){
case MotionEvent.ACTION_DOWN:{
try{
//... ...
final float pressX = ev.getRawX(); //点击位置X
final float pressY = ev.getRawY(); //点击位置Y
findViewAtPosition(myView, (int)pressX, (int)pressY);//找到点击的控件
if(mViewStack.isEmpty()){
return;
}
}catch(Exception e){
Log.e(TAG, "recognizeViewEvent: unknown error");
}
break;
}
case MotionEvent.ACTION_UP:{
//... ...
final float x = ev.getRawX();
final float y = ev.getRawY();
findViewAtPosition(myView, (int)x, (int)y); //找到点击控件
try{
if(view instanceof CheckBox){
//收集控件信息
}else if(view instanceof Button){
//收集控件信息
}else if(view instanceof ImageView){
//收集控件信息
}else if(view instanceof TextView){
//收集控件信息
}else{
}
}catch(Exception e){
Log.e(TAG, "recognizeViewEvent: unknown error");
}
}
break;
}
}
Queen第一步便是识别点击的动作ACTION_DOWN和ACTION_UP,只有两个动作所消费的对象是同一个控件时,此点击才能算是一次完整的点击,然后获取点击位置的x, y。第二步便是根据位置x,y查找相应的View,即findViewAtPosition(View, int, int)方法的调用,找到相应的View以后便进行View的控件类型判断,输出结果。接下来我们看看findViewAtPosition方法都做了一些什么:
/**
* 通过用户动作的范围查找相应的View
* find view that the user interacts.
*
* @param parent 最上层View
* @param x 动作触摸点x坐标
* @param y 动作触摸点y坐标
*/
private void findViewAtPosition(View parent, int x, int y) {
int length = 1;
Rect rect = new Rect();
parent.getGlobalVisibleRect(rect);
if(parent instanceof ViewGroup){
length = ((ViewGroup)parent).getChildCount();
}
for (int i = 0; i < length; i++) {
if(parent instanceof ViewGroup){
if(View.VISIBLE == parent.getVisibility()){
View child = ((ViewGroup) parent).getChildAt(i);
findViewAtPosition(child, x, y);
}
} else {
if (rect.contains(x, y)) {
if(View.VISIBLE == parent.getVisibility() && parent.isClickable()){
mViewStack.push(parent);
}
}
}
}
if(parent.isClickable()
&& rect.contains(x, y)
&& View.VISIBLE == parent.getVisibility()){
mViewStack.push(parent);
}
}
在这个方法中,从根View开始,我们一步步搜索被点击的View,首先确定点击范围rect,然后判断该View是不是ViewGroup,如果是,获取其中包含的子View并对每个子View进行迭代搜索,对于每个子View,我们确定该View是不是点击,同时确定该view是不是在rect点击范围,以及是否可见,最终将符合标准的View放入ViewStack。
通过以上两个方法,Queen一步一步找到用户点击的控件,并记录一条动作信息。这就是Queen寻找屏幕上被点击控件的方法。