(4.5.5.4)Espresso的进阶: AdapterViewProtocol

我们已经知道,对于 AdapterView我们要使用onData()进行数据的匹配,而不再是使用onView()进行 view匹配。

然而在(4.5.5.4)Espresso的进阶: OnView & onData & Matchers中我们也提及了,onData()默认是只支持遵循了 adapter协议的 AdapterView,也就是需要按照 adapter协议 去重写相关接口的函数(尤其是getItem()的API),如果没有,那么就要重写 AdapterViewProtocol。

提醒:在打破了继承约束(尤其是getItem()的API)实现了AdatpterView的自定义view中onData()是有问题的。在这中情况下,做好的做法就是重构应用的代码。如果不重构代码,你也可以实现自定义的AdapterViewProtocol来实现。查看Espresso的AdapterViewProtocols 来查看更多信息。

官方为我们提供了标准的StandardAdapterViewProtocol作为参考:

Rendered 呈现、表达;也就是adapterView显示了指定data对应的view

一、AdapterViewProtocol接口

1.1 getDataInAdapterView()返回全部数据源

  /*
   * Returns all data this AdapterViewProtocol can find within the given AdapterView.
   * 返回当前AdapterViewProtocol在指定的AdapterView上能做到的所有数据,也就是AdapterView的数据源。
   *  且返回的数据的每一个都会被传递到makeDataRenderedWithinView()中
   * @param adapterView 目标AdapterView
   * @return Iterable<AdaptedData>  目标AdapterView的数据源,AdaptedData为数据的封装类型
   */
  Iterable<AdaptedData> getDataInAdapterView(AdapterView<? extends Adapter> adapterView);

我们来看下官方给出的StandardAdapterViewProtocol是如何实现该方法的:

    @Override
    public Iterable<AdaptedData> getDataInAdapterView(AdapterView<? extends Adapter> adapterView) {
      List<AdaptedData> datas = Lists.newArrayList();
      for (int i = 0; i < adapterView.getCount(); i++) {
        int position = i;
        Object dataAtPosition = adapterView.getItemAtPosition(position);
        datas.add(
            new AdaptedData.Builder()
              .withDataFunction(new StandardDataFunction(dataAtPosition, position))
              .withOpaqueToken(position)
              .build());
      }
      return datas;
    }

我们可以看到StandardAdapterViewProtocol就是 根据AdapterView的相关APi函数,构造了一个list数据返回,也就是两个任务:

  • 遍历adpater的数据,并构造AdaptedData填充到list中

需要注意的是,期间使用了 adapterView.getItemAtPosition(position),我们来看getItemAtPosition()的源码:

    public Object getItemAtPosition(int position) {
        T adapter = getAdapter();
        return (adapter == null || position < 0) ? null : adapter.getItem(position);
    }

其实是调用了 adapter的getItem函数,这也就解释了,为什么如果自定义Adapter不遵循协议,为什么就不能适用于StandardAdapterViewProtocol,原因就是不能用常规方式获取到数据源了

  • AdaptedData的成员变量
 new AdaptedData.Builder()
              .withDataFunction(new StandardDataFunction(dataAtPosition, position))
              .withOpaqueToken(position)
              .build()
  1. Object data adapter对应的数据
  2. Object opaqueToken 标示“adapter对应的数据”在adapter中的位置等
  3. DataFunction dataFunction 就是一个回调,可以直接返回data,也可以进行一次转换

先看下AdaptedData.Builder的build()函数,其实很简单,就是 如果dataFunction存在,则使用dataFunction拿到数据;否则直接返回收据:

      public AdaptedData build() {
        if (null != dataFunction) {
          data = dataFunction.getData();
        } else {
          dataFunction = new DataFunction() {
            @Override
            public Object getData() {
              return data;
            }
          };
        }

        return new AdaptedData(data, opaqueToken, dataFunction);
      }

再看下StandardAdapterViewProtocol的dataFunction:

    private static final class StandardDataFunction implements DataFunction {
        private final Object dataAtPosition;
        private final int position;

        private StandardDataFunction(Object dataAtPosition, int position) {
          checkArgument(position >= 0, "position must be >= 0");
          this.dataAtPosition = dataAtPosition;
          this.position = position;
        }

        @Override
        public Object getData() {
          if (dataAtPosition instanceof Cursor) {
            if (!((Cursor) dataAtPosition).moveToPosition(position)) {
              Log.e(TAG, "Cannot move cursor to position: " + position);
            }
          }
          return dataAtPosition;
        }
    }

1.2 getDataRenderedByView获取指定item对应的数据

  /*
   * Returns the data object this particular view is rendering if possible.
   * 返回 指定的item view 所对应的数据
   * 如果是item则返回数据,如果不是则返回Object.absent();
   * @param adapterView 目标AdapterView
   * @param descendantView 指定的item
   * @return Optional<AdaptedData>  对应的单个数据源
   * 
   * 例如:
   * 如果PersonObject实体类被显示在如下item中
   * LinearLayout
   *   ImageView picture
   *   TextView firstName
   *   TextView lastName
   * 那么期望 :
   * - getDataRenderedByView(adapter, LinearLayout)可以返回PersonObject
   * - getDataRenderedByView(adapter, TextView | ImageView )可以返回Object.absent().
   */
  Optional<AdaptedData> getDataRenderedByView(
      AdapterView<? extends Adapter> adapterView, View descendantView);

我们来看下官方给出的StandardAdapterViewProtocol是如何实现该方法的:

    @Override
    public Optional<AdaptedData> getDataRenderedByView(AdapterView<? extends Adapter> adapterView,
        View descendantView) {
      if (adapterView == descendantView.getParent()) {
        int position = adapterView.getPositionForView(descendantView);
        if (position != AdapterView.INVALID_POSITION) {
          return Optional.of(new AdaptedData.Builder()
              .withDataFunction(new StandardDataFunction(adapterView.getItemAtPosition(position),
                      position))
              .withOpaqueToken(Integer.valueOf(position))
              .build());
        }
      }
      return Optional.absent();
    }

也很明显:

  • 如果当前item的父节点就是adapter,则构造对应的数据返回
    • 先获取item view对应的 pos位置,然后根据pos位置获取数据源
    • 同样使用了getItemAtPosition()
  • 否则,返回Optional.absent()

1.3 makeDataRenderedWithinAdapterView 使指定data对应的View显示在adpaterView中

  /*
   * Requests that a particular piece of data held in this AdapterView is actually rendered by it.
   * 使指定data对应的item view显示在adpaterView中.
   *  该函数在getDataRenderedByView()后保证
   *  getDataRenderedByView(adapterView, descView).get() == data.data
   * @param adapterView 目标AdapterView
   * @param AdaptedData 指定的 数据
   * /
  void makeDataRenderedWithinAdapterView(
      AdapterView<? extends Adapter> adapterView, AdaptedData data);

继续看官方给出的StandardAdapterViewProtocol是如何实现该方法的:

   @Override
    public void makeDataRenderedWithinAdapterView(
        AdapterView<? extends Adapter> adapterView, AdaptedData data) {
      checkArgument(data.opaqueToken instanceof Integer, "Not my data: %s", data);
      int position = ((Integer) data.opaqueToken).intValue();//根据AdaptedData 拿到该数据对应的“在adapterView中的位置标示”

      boolean moved = false;//是否滚动过adapterView
      // set selection should always work, we can give a little better experience if per subtype
      // though.
      if (Build.VERSION.SDK_INT > 7) {
        if (adapterView instanceof AbsListView) {
          if (Build.VERSION.SDK_INT > 10) {
            ((AbsListView) adapterView).smoothScrollToPositionFromTop(position,
                adapterView.getPaddingTop(), 0);
          } else {
            ((AbsListView) adapterView).smoothScrollToPosition(position);
          }
          moved = true;
        }
        if (Build.VERSION.SDK_INT > 10) {
          if (adapterView instanceof AdapterViewAnimator) {
            if (adapterView instanceof AdapterViewFlipper) {
              ((AdapterViewFlipper) adapterView).stopFlipping();
            }
            ((AdapterViewAnimator) adapterView).setDisplayedChild(position);
            moved = true;
          }
        }
      }
      if (!moved) {//没滚动过adapterView则选中 所在位置的view
        adapterView.setSelection(position);
      }
    }
  1. 根据AdaptedData 拿到该数据对应的“在adapterView中的位置标示”
  2. 根据 位置表示 进行相关动作譬如:滚动等,使得指定item view显示

1.4 isDataRenderedWithinAdapterView 指定数据的item view是否已经显示

  /*
   * Indicates whether or not there now exists a descendant view within adapterView that is rendering this data..
   * isDataRenderedWithinAdapterView 指定数据的item view是否已经显示
   * @param adapterView 目标AdapterView
   * @param AdaptedData 指定的 数据
   * /
  boolean isDataRenderedWithinAdapterView(
      AdapterView<? extends Adapter> adapterView, AdaptedData adaptedData);

继续看官方给出的StandardAdapterViewProtocol是如何实现该方法的:

 @Override
    public boolean isDataRenderedWithinAdapterView(
        AdapterView<? extends Adapter> adapterView, AdaptedData adaptedData) {
      checkArgument(adaptedData.opaqueToken instanceof Integer, "Not my data: %s", adaptedData);
      int dataPosition = ((Integer) adaptedData.opaqueToken).intValue();根据AdaptedData 拿到该数据对应的“在adapterView中的位置标示”
      boolean inView = false;//是否已经显示

      if (Range.closed(adapterView.getFirstVisiblePosition(), adapterView.getLastVisiblePosition())
          .contains(dataPosition)) {//指定位置 在 adapterView 显示位置范围内
        if (adapterView.getFirstVisiblePosition() == adapterView.getLastVisiblePosition()) {// 空数据
          // thats a huge element.
          inView = true;
        } else {//是否是完全显示
          inView = isElementFullyRendered(adapterView,
              dataPosition - adapterView.getFirstVisiblePosition());
        }
      }
      if (inView) {
        // stops animations - locks in our x/y location.
        adapterView.setSelection(dataPosition);
      }

      return inView;
    }
    //指定位置的item View 是否在adapterView中完全显示
    private boolean isElementFullyRendered(AdapterView<? extends Adapter> adapterView,
        int childAt) {
      View element = adapterView.getChildAt(childAt);
      // Occassionally we'll have to fight with smooth scrolling logic on our definition of when
      // there is extra scrolling to be done. In particular if the element is the first or last
      // element of the list, the smooth scroller may decide that no work needs to be done to scroll
      // to the element if a certain percentage of it is on screen. Ugh. Sigh. Yuck.

      return isDisplayingAtLeast(FULLY_RENDERED_PERCENTAGE_CUTOFF).matches(element);
    }
  }
  1. 拿到该数据对应的“在adapterView中的位置标示”
  2. 判断 指定位置的item view 是否 在 adapterView 显示位置范围内
    • 是,则校验 A.list空数据 B.item完全显示

到此我们基本已经了解了AdapterViewProtocol接口和官方默认StandardAdapterViewProtocol的实现原理,下面就让我们自定一下吧;

二、 CursorAdapterViewProtocol


/**
 * 类描述:
 *
 * Created by yhf on 2016/11/29.
 */
public class CursorAdapterViewProtocol implements AdapterViewProtocol {

    private static final int FULLY_RENDERED_PERCENTAGE_CUTOFF = 90;

    public Object getDataFromCursor(CursorAdapter cursorAdapter, Cursor cursor) {
        return cursorAdapter.convertToString(cursor);
    }

    @Override
    public Iterable<AdaptedData> getDataInAdapterView(AdapterView<? extends Adapter> adapterView) {
        CursorAdapter adapter = (CursorAdapter) adapterView.getAdapter();

        List<AdaptedData> datas = Lists.newArrayList();
        for (int i = 0; i < adapterView.getCount(); i++) {
            Cursor cursor = (Cursor) adapterView.getItemAtPosition(i);
            datas.add(new AdaptedData.Builder()
                    .withData(getDataFromCursor(adapter, cursor))
                    .withOpaqueToken(i)
                    .build());
        }
        return datas;
    }

    @Override
    public Optional<AdaptedData> getDataRenderedByView(AdapterView<? extends Adapter> adapterView, View descendantView) {
        if (adapterView == descendantView.getParent()) {
            int position = adapterView.getPositionForView(descendantView);
            if (position != AdapterView.INVALID_POSITION) {
                CursorAdapter adapter = (CursorAdapter) adapterView.getAdapter();
                Cursor cursor = (Cursor) adapterView.getItemAtPosition(position);
                return Optional.of(new AdaptedData.Builder()
                        .withData(getDataFromCursor(adapter, cursor))
                        .withOpaqueToken(Integer.valueOf(position))
                        .build());
            }
        }
        return Optional.absent();
    }

    @Override
    public void makeDataRenderedWithinAdapterView(AdapterView<? extends Adapter> adapterView, AdaptedData data) {
        checkArgument(data.opaqueToken instanceof Integer, "Not my data: %s", data);
        int position = ((Integer) data.opaqueToken).intValue();

        boolean moved = false;
        // set selection should always work, we can give a little better experience if per subtype
        // though.
        if (Build.VERSION.SDK_INT > 7) {
            if (adapterView instanceof AbsListView) {
                if (Build.VERSION.SDK_INT > 10) {
                    ((AbsListView) adapterView).smoothScrollToPositionFromTop(position,
                            adapterView.getPaddingTop(), 0);
                } else {
                    ((AbsListView) adapterView).smoothScrollToPosition(position);
                }
                moved = true;
            }
            if (Build.VERSION.SDK_INT > 10) {
                if (adapterView instanceof AdapterViewAnimator) {
                    if (adapterView instanceof AdapterViewFlipper) {
                        ((AdapterViewFlipper) adapterView).stopFlipping();
                    }
                    ((AdapterViewAnimator) adapterView).setDisplayedChild(position);
                    moved = true;
                }
            }
        }
        if (!moved) {
            adapterView.setSelection(position);
        }
    }

    @SuppressWarnings("deprecation")
    @Override
    public boolean isDataRenderedWithinAdapterView(AdapterView<? extends Adapter> adapterView, AdaptedData adaptedData) {
        checkArgument(adaptedData.opaqueToken instanceof Integer, "Not my data: %s", adaptedData);
        int dataPosition = ((Integer) adaptedData.opaqueToken).intValue();

        if (Range.closed(adapterView.getFirstVisiblePosition(), adapterView.getLastVisiblePosition()).contains(dataPosition)) {
            if (adapterView.getFirstVisiblePosition() == adapterView.getLastVisiblePosition()) {
                // thats a huge element.
                return true;
            } else {
                return isElementFullyRendered(adapterView, dataPosition - adapterView.getFirstVisiblePosition());
            }
        } else {
            return false;
        }
    }

    private boolean isElementFullyRendered(AdapterView<? extends Adapter> adapterView, int childAt) {
        View element = adapterView.getChildAt(childAt);
        // Occassionally we'll have to fight with smooth scrolling logic on our definition of when
        // there is extra scrolling to be done. In particular if the element is the first or last
        // element of the list, the smooth scroller may decide that no work needs to be done to scroll
        // to the element if a certain percentage of it is on screen. Ugh. Sigh. Yuck.

        return isDisplayingAtLeast(FULLY_RENDERED_PERCENTAGE_CUTOFF).matches(element);
    }
}

三、自定义AdapterViewProtocol实例

/**
 * 类描述:
 * 满足一些 adpater没有按照规则实现的时候,使用OnData();
 * 主要就是 实例化具体类型的Adapter,并调用自定义的getitem()方法
 * Created by yhf on 2016/11/29.
 */
public abstract class BaseListAdapterViewProtocol<T extends Adapter> implements AdapterViewProtocol {

    private static final int FULLY_RENDERED_PERCENTAGE_CUTOFF = 90;

    /**
     * 子类重新
     */
    public abstract Object getDataFromCusTomer(T myAdapter, int pos);

    public Object getDataFromCursor(T myAdapter, int pos){
     if(pos == -1){
            return null;
     }else if(pos >=  myAdapter.getCount()){
         return null;
     }else{
        return getDataFromCusTomer(myAdapter, pos);
     }
    }

    public int getHeaderCount(AdapterView<? extends Adapter> adapterView){

        if(adapterView instanceof AbsListView){
            Log.i("AdapterViewProtocol", ""+((ListView)adapterView).getHeaderViewsCount());
            return ((ListView)adapterView).getHeaderViewsCount();
        }

        return 0;

    }

    @Override
    public Iterable<AdaptedData> getDataInAdapterView(AdapterView<? extends Adapter> adapterView) {
        T adapter = null;
        if( adapterView.getAdapter() instanceof HeaderViewListAdapter){
            adapter = (T) ( (HeaderViewListAdapter) adapterView.getAdapter()).getWrappedAdapter();
        } else{
            adapter = (T) adapterView.getAdapter();
        }

        List<AdaptedData> datas = Lists.newArrayList();
        for (int i = 0; i < getHeaderCount(adapterView); i++) {
            datas.add(new AdaptedData.Builder()
                    .withData(null)
                    .withOpaqueToken(-1)
                    .build());
        }
        for (int i = 0; i < (adapter.getCount()); i++) {
            datas.add(new AdaptedData.Builder()
                    .withData(getDataFromCursor(adapter, i))
                    .withOpaqueToken(i + getHeaderCount(adapterView))
                    .build());
        }
        return datas;
    }

    @Override
    public Optional<AdaptedData> getDataRenderedByView(AdapterView<? extends Adapter> adapterView, View descendantView) {
        if (adapterView == descendantView.getParent()) {
            int position = adapterView.getPositionForView(descendantView);
            if (position != AdapterView.INVALID_POSITION) {
                T adapter = null;
                if( adapterView.getAdapter() instanceof HeaderViewListAdapter){
                    adapter = (T) ( (HeaderViewListAdapter) adapterView.getAdapter()).getWrappedAdapter();
                } else{
                    adapter = (T) adapterView.getAdapter();
                }
                return Optional.of(new AdaptedData.Builder()
                        .withData(getDataFromCursor(adapter, position - getHeaderCount(adapterView)))
                        .withOpaqueToken(Integer.valueOf(position))
                        .build());
            }
        }
        return Optional.absent();
    }

    @Override
    public void makeDataRenderedWithinAdapterView(AdapterView<? extends Adapter> adapterView, AdaptedData data) {
        checkArgument(data.opaqueToken instanceof Integer, "Not my data: %s", data);
        int position = ((Integer) data.opaqueToken).intValue();

        boolean moved = false;
        // set selection should always work, we can give a little better experience if per subtype
        // though.
        if (Build.VERSION.SDK_INT > 7) {
            if (adapterView instanceof AbsListView) {
                if (Build.VERSION.SDK_INT > 10) {
                    ((AbsListView) adapterView).smoothScrollToPositionFromTop(position,
                            adapterView.getPaddingTop(), 0);
                } else {
                    ((AbsListView) adapterView).smoothScrollToPosition(position);
                }
                moved = true;
            }
            if (Build.VERSION.SDK_INT > 10) {
                if (adapterView instanceof AdapterViewAnimator) {
                    if (adapterView instanceof AdapterViewFlipper) {
                        ((AdapterViewFlipper) adapterView).stopFlipping();
                    }
                    ((AdapterViewAnimator) adapterView).setDisplayedChild(position);
                    moved = true;
                }
            }
        }
        if (!moved) {
            adapterView.setSelection(position);
        }
    }

    @SuppressWarnings("deprecation")
    @Override
    public boolean isDataRenderedWithinAdapterView(AdapterView<? extends Adapter> adapterView, AdaptedData adaptedData) {
        checkArgument(adaptedData.opaqueToken instanceof Integer, "Not my data: %s", adaptedData);
        int dataPosition = ((Integer) adaptedData.opaqueToken).intValue();

        if (Range.closed(adapterView.getFirstVisiblePosition(), adapterView.getLastVisiblePosition()).contains(dataPosition)) {
            if (adapterView.getFirstVisiblePosition() == adapterView.getLastVisiblePosition()) {
                // thats a huge element.
                return true;
            } else {
                return isElementFullyRendered(adapterView, dataPosition - adapterView.getFirstVisiblePosition());
            }
        } else {
            return false;
        }
    }

    private boolean isElementFullyRendered(AdapterView<? extends Adapter> adapterView, int childAt) {
        View element = adapterView.getChildAt(childAt);
        // Occassionally we'll have to fight with smooth scrolling logic on our definition of when
        // there is extra scrolling to be done. In particular if the element is the first or last
        // element of the list, the smooth scroller may decide that no work needs to be done to scroll
        // to the element if a certain percentage of it is on screen. Ugh. Sigh. Yuck.

        return isDisplayingAtLeast(FULLY_RENDERED_PERCENTAGE_CUTOFF).matches(element);
    }
}
public class LegwrkVisitListAdapterViewProtocol extends BaseListAdapterViewProtocol<BaseLegWrkListActivity.BaseLegwrkListAdapter> {
    @Override
    public Object getDataFromCusTomer(BaseLegWrkListActivity.BaseLegwrkListAdapter myAdapter, int pos) {
        return myAdapter.getLegWorkLineVo(pos);
    }
}
        onData(LegWorkMatcher.searchMainItemWithName(targetTitle)).usingAdapterViewProtocol(new LegwrkVisitListAdapterViewProtocol()).perform(click());
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值