package com.google.android.apps.common.testing.ui.espresso.action; import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isDisplayingAtLeast; import static com.google.common.base.Preconditions.checkArgument; import com.google.common.base.Optional; import com.google.common.collect.Lists; import com.google.common.collect.Range; import android.os.Build; import android.view.View; import android.widget.AbsListView; import android.widget.Adapter; import android.widget.AdapterView; import android.widget.AdapterViewAnimator; import android.widget.AdapterViewFlipper; import java.util.List; /** * Implementations of {@link AdapterViewProtocol} for standard SDK Widgets. * */ public final class AdapterViewProtocols { /** * Consider views which have over this percentage of their area visible to the user * to be fully rendered. */ private static final int FULLY_RENDERED_PERCENTAGE_CUTOFF = 90; private AdapterViewProtocols() {} private static final AdapterViewProtocol STANDARD_PROTOCOL = new StandardAdapterViewProtocol(); /** * Creates an implementation of AdapterViewProtocol that can work with AdapterViews that do not * break method contracts on AdapterView. * */ public static AdapterViewProtocol standardProtocol() { return STANDARD_PROTOCOL; } // TODO(user): expandablelistview protocols private static final class StandardAdapterViewProtocol implements AdapterViewProtocol { @Override public Iterable<AdaptedData> getDataInAdapterView(AdapterView<? extends Adapter> adapterView) { List<AdaptedData> datas = Lists.newArrayList(); for (int i = 0; i < adapterView.getCount(); i++) { datas.add( new AdaptedData.Builder() .withData(adapterView.getItemAtPosition(i)) .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) { return Optional.of(new AdaptedData.Builder() .withData(adapterView.getItemAtPosition(position)) .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); } } }