我们已经知道,对于 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()
- Object data adapter对应的数据
- Object opaqueToken 标示“adapter对应的数据”在adapter中的位置等
- 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);
}
}
- 根据AdaptedData 拿到该数据对应的“在adapterView中的位置标示”
- 根据 位置表示 进行相关动作譬如:滚动等,使得指定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);
}
}
- 拿到该数据对应的“在adapterView中的位置标示”
- 判断 指定位置的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());