public final class

AdapterViewProtocols

extends java.lang.Object

 java.lang.Object

↳androidx.test.espresso.action.AdapterViewProtocols

Gradle dependencies

compile group: 'androidx.test.espresso', name: 'espresso-core', version: '3.6.1'

  • groupId: androidx.test.espresso
  • artifactId: espresso-core
  • version: 3.6.1

Artifact androidx.test.espresso:espresso-core:3.6.1 it located at Google repository (https://maven.google.com/)

Androidx artifact mapping:

androidx.test.espresso:espresso-core com.android.support.test.espresso:espresso-core

Androidx class mapping:

androidx.test.espresso.action.AdapterViewProtocols android.support.test.espresso.action.AdapterViewProtocols

Overview

Implementations of AdapterViewProtocol for standard SDK Widgets.

Summary

Methods
public static AdapterViewProtocolstandardProtocol()

Creates an implementation of AdapterViewProtocol that can work with AdapterViews that do not break method contracts on AdapterView.

from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Methods

public static AdapterViewProtocol standardProtocol()

Creates an implementation of AdapterViewProtocol that can work with AdapterViews that do not break method contracts on AdapterView.

Source

/*
 * Copyright (C) 2014 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.test.espresso.action;

import static androidx.test.espresso.matcher.ViewMatchers.isDisplayingAtLeast;
import static androidx.test.internal.util.Checks.checkArgument;

import android.database.Cursor;
import android.util.Log;
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 androidx.annotation.Nullable;
import androidx.test.espresso.util.EspressoOptional;
import java.util.ArrayList;
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;
  }

  private static final class StandardAdapterViewProtocol implements AdapterViewProtocol {

    private static final String TAG = "StdAdapterViewProtocol";

    /** Required for Espresso remote serialization, called reflectively. */
    public StandardAdapterViewProtocol() {}

    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;
      }
    }

    @Override
    public Iterable<AdaptedData> getDataInAdapterView(AdapterView<? extends Adapter> adapterView) {
      List<AdaptedData> datas = new ArrayList<>();
      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;
    }

    /**
     * @deprecated use {@link #getDataRenderedByView2(AdapterView, View)}
     */
    @Override
    @Deprecated
    public EspressoOptional<AdaptedData> getDataRenderedByView(
        AdapterView<? extends Adapter> adapterView, View descendantView) {
      return EspressoOptional.of(getDataRenderedByView2(adapterView, descendantView));
    }

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

    @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 (adapterView instanceof AbsListView) {
        ((AbsListView) adapterView)
            .smoothScrollToPositionFromTop(position, adapterView.getPaddingTop(), 0);

        moved = true;
      }

      if (adapterView instanceof AdapterViewAnimator) {
        if (adapterView instanceof AdapterViewFlipper) {
          ((AdapterViewFlipper) adapterView).stopFlipping();
        }
        ((AdapterViewAnimator) adapterView).setDisplayedChild(position);
        moved = true;
      }
      if (!moved) {
        adapterView.setSelection(position);
      }
    }

    @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();
      boolean inView = false;

      if (dataPosition >= adapterView.getFirstVisiblePosition()
          && dataPosition <= adapterView.getLastVisiblePosition()) {
        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;
    }

    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);
    }
  }
}