Robotium 5.0.1 源码解析之控件搜索

转自:http://bbs.51cto.com/thread-1116630-1.html

自己和Android的自动化测试已经打了3年交道有余,却一直没有详细了解一下robotium,最近终于抽出时间阅读了其源码,把收获好好记录一番。

     众所周知,Robotium是基于Android的单元测试框架Instrumentation,而robotium对于Instrumentation封装的比较强的地方便是控件搜索,因此首先先来了解一下在robotium中控件的搜索原理,这部分的源码主要位于ViewFetcher.java中。

     1.mViews的获取    要先搜索控件,必须先得到Activity的rootView。在Android中,对于一般的Activity或其对话框,其rootView叫做DecorView,其实就是Activity和Dialog外面的那层框(关于Activity或dialog的层次可以用HierarchyViewer来查看)。


   虽然通过Activity类的getWindow().getDecorView可以获取到Activity自身的DecorView,但是无法获取到对话框的,因此Robotium中界面控件是从WindowManagerGlobal(或WindowManagerImpl)中的mViews获取到的。当然mViews中不但包含DecorView,还包含同进程内的所有界面的根节(如悬浮框的根节点)。mView的值的获取过程主要如下:

   1) 确定mViews所在类:android 4.2之前,获取类为android.view.WindowManagerImpl,4.2及之后,获取类为WindowManagerGlobal

Java代码  [url=][/url]

  • String windowManagerClassName;  
  • if (android.os.Build.VERSION.SDK_INT >= 17) {  
  •         windowManagerClassName = "android.view.WindowManagerGlobal";  
  • else {  
  •         windowManagerClassName = "android.view.WindowManagerImpl";  
  • }  
  • windowManager = Class.forName(windowManagerClassName)  



    2). 获得类的实例:此类是个单例类,有直接的静态变量可以获取到其实例, 4.2及之后的版本其变量名为sDefaultWindowManager,3.2至4.1,其变量名为sWindowManager,3.2之前,其变量名为mWindowManager。


Java代码  [url=][/url]

  • /**
  • * Sets the window manager string.
  • */  
  • private void setWindowManagerString(){  
  •          if (android.os.Build.VERSION.SDK_INT >= 17) {  
  •                   windowManagerString = "sDefaultWindowManager";  
  •          } else if(android.os.Build.VERSION.SDK_INT >= 13) {  
  •                   windowManagerString = "sWindowManager";  
  •          } else {  
  •                   windowManagerString = "mWindowManager";  
  •          }  
  • }  


    3). 获取mViews变量的值了,从4.4开始类型变为ArrayList<View>,之前为View[]
Java代码  [url=][/url]

  • viewsField = windowManager.getDeclaredField("mViews");  
  • instanceField = windowManager.getDeclaredField(windowManagerString);  
  • viewsField.setAccessible(true);  
  • instanceField.setAccessible(true);  
  • Object instance = instanceField.get(null);  
  • View[] result;  
  • if (android.os.Build.VERSION.SDK_INT >= 19) {  
  •           result = ((ArrayList<View>) viewsField.get(instance)).toArray(new View[0]);  
  • else {  
  •           result = (View[]) viewsField.get(instance);  
  • }  



  2.mViews的过滤   mViews中会包含三种类型的View:
   1) 当前显示的以及没有显示的Activity的DecorView
   2) 当前对话框的DecorView
   3) 悬浮框View等其他不属于DecorView的独立View

     在搜索控件时,显然需要在最上层界面中搜索,所以搜索范围为: 
               最上层的Activity/Dialog + 悬浮框

     对于悬浮框,robotium中的处理是找出mViews中不属于DecorView类的View,并将其所有子控件引入。
Java代码  [url=][/url]

  • private final View[] getNonDecorViews(View[] views) {  
  •          View[] decorViews = null;  
  •          if(views != null) {  
  •              decorViews = new View[views.length];  
  •              int i = 0;  
  •              View view;  
  •              for (int j = 0; j < views.length; j++) {  
  •                  view = views[j];  
  •                  if (view != null && !(view.getClass().getName()  
  •                          .equals("com.android.internal.policy.impl.PhoneWindow$DecorView"))) {  
  •                      decorViews = view;  
  •                      i++;  
  •                  }  
  •              }  
  •          }  
  •          return decorViews;  
  •      }  


    对于Activity/Dialog的筛选,Robotium采取对比DrawingTime的方法选出最后绘制的DecorView,其即为最上层Activity/Dialog的DecorView:
Java代码  [url=][/url]

  • /**
  •      * Returns the most recent view container
  •      *
  •      * @param views the views to check
  •      * @return the most recent view container
  •      */  
  •      private final View getRecentContainer(View[] views) {  
  •          View container = null;  
  •          long drawingTime = 0;  
  •          View view;  
  •          for(int i = 0; i < views.length; i++){  
  •              view = views;  
  •              if (view != null && view.isShown() && view.hasWindowFocus() && view.getDrawingTime() > drawingTime) {  
  •                  container = view;  
  •                  drawingTime = view.getDrawingTime();  
  •              }  
  •          }  
  •          return container;  
  •      }  



  3.控件过滤&控件列表生成     得到悬浮框的根节点和最上层的DecorView后,robotium会将所有View统一添加到一个ArrayList中生成控件列表。添加方法本身很简单,就是一个简单的递归,但需要注意的是此处有一个onlySufficientlyVisible的判断。onlySufficientlyVisibleViewFetcher中最常见的一个变量,其表示是否过滤掉显示不完全的控件,即onlySufficientlyVisibletrue时表示只在显示完全的控件中搜索目标,为false时表示在所有控件中搜索目标。具体代码为下面的addChildren函数:


Java代码  [url=][/url]

  • private void addChildren(ArrayList<View> views, ViewGroup viewGroup, boolean onlySufficientlyVisible) {  
  •         if(viewGroup != null){  
  •             for (int i = 0; i < viewGroup.getChildCount(); i++) {  
  •                 final View child = viewGroup.getChildAt(i);  
  •                 if(onlySufficientlyVisible && isViewSufficientlyShown(child))  
  •                     views.add(child);  
  •                 else if(!onlySufficientlyVisible)  
  •                     views.add(child);  
  •                 if (child instanceof ViewGroup) {  
  •                     addChildren(views, (ViewGroup) child, onlySufficientlyVisible);  
  •                 }  
  •             }  
  •         }  
  •     }  

从上面的代码可以看出,当onlySufficientlyVisible true 时,robotium 会对控件的可见不可见进行检查。不过这里的可见不可见不是指Visible Invisible Robotium 过滤Invisible 控件的方法是RobotiumUtils.removeInvisibleViews, 原理是利用view.isShown() 方法),而是指由于界面滚动而导致的没有显示或显示不完全。继续看Robotium SufficientlyVisible 是怎么判断的:

Java代码  [url=][/url]

  • public final boolean isViewSufficientlyShown(View view){  
  •         final int[] xyView = new int[2];  
  •         final int[] xyParent = new int[2];  
  •         if(view == null)  
  •             return false;  
  •         final float viewHeight = view.getHeight();  
  •         final View parent = getScrollOrListParent(view);  
  •         view.getLocationOnScreen(xyView);  
  •         if(parent == null){  
  •             xyParent[1] = 0;  
  •         }  
  •         else{  
  •             parent.getLocationOnScreen(xyParent);  
  •         }  
  •         if(xyView[1] + (viewHeight/2.0f) > getScrollListWindowHeight(view))  
  •             return false;  
  •         else if(xyView[1] + (viewHeight/2.0f) < xyParent[1])  
  •             return false;  
  •         return true;  
  •     }  

    
    代码中getScrollOrListParent是获取控件所属的ListView或ScrollView,可能是控件本身也可能是空。getScrollListWindowHeight函数用于获取控件所属的ListView或ScrollView最下面边界的Y坐标。因此Java代码  [url=][/url]

  • xyView[1] + (viewHeight/2.0f) > getScrollListWindowHeight(view)  

这个判断就表示控件有超过一半的面积被隐藏在了父控件的下方,而
Java代码  [url=][/url]

  • (xyView[1] + (viewHeight/2.0f) < xyParent[1]  


则表示控件有超过一半的面积被隐藏在了父控件的上方,这两种情况都被Robotium判断为不满足SufficientlyVisible的(不过好像没有判断横向的?)。

根据onlySufficientlyVisible过滤掉相应控件后,robotium便完成了控件列表的生成工作,之后的搜索就可直接在列表中进行查找了。

有的时候要搜索指定类型的控件,可以按照类型对控件列表进行再一次的过滤,ViewFetcher中的代码如下:

Java代码  [url=][/url]

  • public <T extends View> ArrayList<T> getCurrentViews(Class<T> classToFilterBy, View parent) {  
  •           ArrayList<T> filteredViews = new ArrayList<T>();  
  •           List<View> allViews = getViews(parent, true);  
  •           for(View view : allViews){  
  •                  if (view != null && classToFilterBy.isAssignableFrom(view.getClass())) {  
  •                         filteredViews.add(classToFilterBy.cast(view));  
  •                  }  
  •           }  
  •           allViews = null;  
  •           return filteredViews;  
  •    }  


可以看到,robotium直接利用了Class. isAssignableFrom进行类型的匹配。

  4.文本搜索获得了控件列表,可以开始搜索指定的目标控件了,先从我们最常用的文本搜索开始,看看robotium的搜索流程。搜索过程的代码主要位于Searcher.java中,主要功能在两个searchFor函数中实现,通过嵌套完成目标的搜索。
第一层

Java代码  [url=][/url]

  • <strong>    public <T extends TextView> T searchFor(final Class<T> viewClass, final String regex, intexpectedMinimumNumberOfMatches, final long timeout, final boolean scroll, final boolean onlyVisible) {  
  •         //修正非法的expectedMinimumNumberOfMatches  
  •         if(expectedMinimumNumberOfMatches < 1) {  
  •             expectedMinimumNumberOfMatches = 1;  
  •         }  
  •         //定义一个Callable给下层searchFor使用,可以直接获取到符合条件的控件列表  
  •         final Callable<Collection<T>> viewFetcherCallback = new Callable<Collection<T>>() {  
  •             @SuppressWarnings("unchecked")  
  •             public Collection<T> call() throws Exception {  
  •                 sleeper.sleep();  
  •                 //从当前的Android View中获取到符合viewClass的控件列表  
  •                 ArrayList<T> viewsToReturn = viewFetcher.getCurrentViews(viewClass);  
  •                 if(onlyVisible){  
  •                     //过滤掉Invisible的控件  
  •                     viewsToReturn = RobotiumUtils.removeInvisibleViews(viewsToReturn);  
  •                 }  
  •                 //robotium支持在webView中查找网页控件,因此若目标控件是TextView或是TextView的子类,  
  •                 //会把网页中的文本框也加到控件列表中。  
  •                 if(viewClass.isAssignableFrom(TextView.class)) {  
  •                     viewsToReturn.addAll((Collection<? extends T>) webUtils.getTextViewsFromWebView());  
  •                 }  
  •                 return viewsToReturn;  
  •             }  
  •         };  
  •         try {  
  •             //调用下层searchFor继续搜索  
  •             return searchFor(viewFetcherCallback, regex, expectedMinimumNumberOfMatches, timeout, scroll);  
  •         } catch (Exception e) {  
  •             throw new RuntimeException(e);  
  •         }  
  •     }  
  • </strong>  


   这个函数的主要功能有二,一是对非法的expectedMinimumNumberOfMatches进行修正,二是为下一层searchFor提供一个Callable,里面定义好了控件列表的获取过程。

   1)      expectedMinimumNumberOfMatches:这个参数表示搜索目标最小发现数目,当一个界面中有多个控件满足搜索条件,通过此参数可以指定想要获取的是第几个。

  2)      Callable<Collection<T>> viewFetcherCallback:定义了控件列表(即搜索范围)的获取过程。首先利用前面提到的viewFetcher.getCurrentViews(viewClass)获取一个初步的列表;再通过RobotiumUtils.removeInvisibleViews(viewsToReturn)过滤掉不可见控件;最后由于Robotium支持webView内部搜索(Robotium的名字貌似也是来源于Selenium),所以当搜索目标是一个TextView时,Robotium还会调用webUtils.getTextViewsFromWebView()把网页中的文本框加入到搜索范围中。

第二层

Java代码  [url=][/url]

  • <strong>    public <T extends TextView> T searchFor(Callable<Collection<T>> viewFetcherCallback, String regex, int expectedMinimumNumberOfMatches, long timeout, boolean scroll) throws Exception {  
  •         final long endTime = SystemClock.uptimeMillis() + timeout;   
  •         Collection<T> views;  
  •         while (true) {  
  •             final boolean timedOut = timeout > 0 && SystemClock.uptimeMillis() > endTime;  
  •             if(timedOut){  
  •                 logMatchesFound(regex);  
  •                 return null;  
  •             }  
  •             //获取符合条件的控件列表  
  •             views = viewFetcherCallback.call();  
  •             for(T view : views){  
  •                 if (RobotiumUtils.getNumberOfMatches(regex, view, uniqueTextViews) == expectedMinimumNumberOfMatches) {  
  •                     uniqueTextViews.clear();  
  •                     return view;  
  •                 }  
  •             }  
  •             if(scroll && !scroller.scrollDown()){  
  •                 logMatchesFound(regex);  
  •                 return null;   
  •             }  
  •             if(!scroll){  
  •                 logMatchesFound(regex);  
  •                 return null;   
  •             }  
  •         }  
  •     }</strong>  


    这一层的主要功能就是循环在控件列表中找到含有指定文本的控件,直至超时或发现了   expectedMinimumNumberOfMatches数目的目标控件,这个过程中需要注意的有四点:
1)    uniqueTextViews:为了防止找到的控件存在重复,此处用了一个uniqueTextViews集合来存储搜索到的结果。
   2)    文本的匹配:直接利用了Pattern进行正则匹配,但比对的内容不只包括view.getText(),还包括 view.getError()以及view.getHint()
   3)    自动滚动:当开启了scroll选项,并且在当前的界面没有找到足够的目标时,Robotium会自动滚动界面 (不过好像只会向下?):
Java代码  [url=][/url]

  • if(scroll && !scroller.scrollDown()  

   4)     滚动时robotium只会滚动drawingTime最大的控件(通过ViewFetcher.getFreshestView()),所以一个界面中有两个可滚动控件时,robotium只会滚动其中一个。

int[] location = new  int[2] ;

/**获取在当前窗口内的绝对坐标,getLeft , getTop, getBottom, getRight,  这一组是获取相对在它父窗口里的坐标。*/
view.getLocationInWindow(location); 
//获取在整个屏幕内的绝对坐标,注意这个值是要从屏幕顶端算起,也就是包括了通知栏的高度。
view.getLocationOnScreen(location);


其中 location [0]代表x坐标,location [1] 代表 坐标。

所以在需要确定组件在父窗体中的坐标时,使用getLocationInWindow,需要获得组件在整个屏幕的坐标时,使用getLocationOnScreen。
这里要注意虽然getLocationOnScreen是获取组件在屏幕中的坐标,但如果我们想拿到这个坐标,并且在这个坐标附近再添加一个组件时,直接使用拿到的坐标来建立新的组件是达不到效果的。

  View itemView = userManagerView.getListView().getChildAt(j);// 获取列表子项
  int[] location = new int[2];
  itemView.getLocationOnScreen(location);
  ImageView image = new ImageView(getContext());

  image.setBackgroundResource(R.drawable.operator);
  if (GlobalData.loginState()) {
   params = new AbsoluteLayout.LayoutParams(LayoutParams.WRAP_CONTENT,
     LayoutParams.WRAP_CONTENT, location[0]-15, location[1] - 95);
  } else {
   params = new AbsoluteLayout.LayoutParams(LayoutParams.WRAP_CONTENT,
     LayoutParams.WRAP_CONTENT, location[0]-15, location[1] - 25);
  }
  image.setLayoutParams(params);


在代码中,我想根据itemVIew的坐标来绘制一个新的ImageView控件,但如果整个界面没有父容器时,拿到的坐标是可以直接使用的,而当前界面上有父容器,或者有与其平行的界面时,坐标是有偏移的,所以我加上了有些判断处理,根据不同的布局,适当的调整下坐标的偏移量

if (GlobalData.loginState()) {
   params = new AbsoluteLayout.LayoutParams(LayoutParams.WRAP_CONTENT,
     LayoutParams.WRAP_CONTENT, location[0]-15, location[1] - 95);
  } else {
   params = new AbsoluteLayout.LayoutParams(LayoutParams.WRAP_CONTENT,
     LayoutParams.WRAP_CONTENT, location[0]-15, location[1] - 25);
  }

1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看REAdMe.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看REAdMe.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看READme.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。
1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 、 1资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看READmE.文件(md如有),本项目仅用作交流学习参考,请切勿用于商业用途。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值