Merge "adding 'thatFirstMatches' view matcher to identify first view that matches by given matcher" into android-support-test
diff --git a/espresso/core-tests/src/androidTest/java/android/support/test/espresso/matcher/ViewMatchersTest.java b/espresso/core-tests/src/androidTest/java/android/support/test/espresso/matcher/ViewMatchersTest.java
index 4781a9b..ebdf077 100644
--- a/espresso/core-tests/src/androidTest/java/android/support/test/espresso/matcher/ViewMatchersTest.java
+++ b/espresso/core-tests/src/androidTest/java/android/support/test/espresso/matcher/ViewMatchersTest.java
@@ -36,6 +36,7 @@
 import static android.support.test.espresso.matcher.ViewMatchers.isRoot;
 import static android.support.test.espresso.matcher.ViewMatchers.isSelected;
 import static android.support.test.espresso.matcher.ViewMatchers.supportsInputMethods;
+import static android.support.test.espresso.matcher.ViewMatchers.thatMatchesFirst;
 import static android.support.test.espresso.matcher.ViewMatchers.withChild;
 import static android.support.test.espresso.matcher.ViewMatchers.withContentDescription;
 import static android.support.test.espresso.matcher.ViewMatchers.withEffectiveVisibility;
@@ -58,6 +59,7 @@
 import android.content.Context;
 import android.graphics.Color;
 import android.os.Build;
+import android.support.annotation.NonNull;
 import android.support.test.annotation.UiThreadTest;
 import android.support.test.espresso.matcher.ViewMatchers.Visibility;
 import android.support.test.rule.UiThreadTestRule;
@@ -726,6 +728,18 @@
   }
 
   @Test
+  public void firstMatchesByGivenMatcher() {
+    View firstViewWithId1 = createViewWithId(R.id.testId1);
+    View secondViewWithId1 = createViewWithId(R.id.testId1);
+
+    Matcher<View> id1FirstOccurrenceMatcher = thatMatchesFirst(withId(R.id.testId1));
+
+    assertTrue(id1FirstOccurrenceMatcher.matches(firstViewWithId1));
+    assertFalse(id1FirstOccurrenceMatcher.matches(secondViewWithId1));
+    assertTrue(id1FirstOccurrenceMatcher.matches(firstViewWithId1));
+  }
+
+  @Test
   public void withInputType_ReturnsTrueIf_CorrectInput() {
     EditText editText = new EditText(context);
     editText.setInputType(InputType.TYPE_CLASS_NUMBER);
@@ -746,4 +760,10 @@
     assertFalse(withInputType(UNRECOGNIZED_INPUT_TYPE).matches(editText));
   }
 
-}
\ No newline at end of file
+  @NonNull
+  private View createViewWithId(int viewId) {
+    View view = new View(context);
+    view.setId(viewId);
+    return view;
+  }
+}
diff --git a/espresso/core/src/main/java/android/support/test/espresso/matcher/ViewMatchers.java b/espresso/core/src/main/java/android/support/test/espresso/matcher/ViewMatchers.java
index efb9c0f..405de4f 100644
--- a/espresso/core/src/main/java/android/support/test/espresso/matcher/ViewMatchers.java
+++ b/espresso/core/src/main/java/android/support/test/espresso/matcher/ViewMatchers.java
@@ -1205,4 +1205,40 @@
       }
     };
   }
+
+  /**
+   * Returns a matcher that matches <b>first<b> {@link android.view.View} by
+   * given matcher.
+   *
+   * If there are multiple views with the same attributes present in view hierarchy, this matcher
+   * will only identify the first view from the view hierarchy.
+   *
+   * @param viewMatcher to match the view.
+   */
+  public static Matcher<View> thatMatchesFirst(final Matcher<View> viewMatcher) {
+    return new TypeSafeMatcher<View>() {
+
+      private boolean isFirstViewFound;
+
+      private View matchedView;
+
+      @Override
+      protected boolean matchesSafely(View view) {
+        if (isFirstViewFound) {
+          return matchedView == view;
+        }
+        isFirstViewFound = viewMatcher.matches(view);
+        if (isFirstViewFound) {
+          matchedView = view;
+        }
+        return isFirstViewFound;
+      }
+
+      @Override
+      public void describeTo(Description description) {
+        description.appendText("that matches first");
+        viewMatcher.describeTo(description);
+      }
+    };
+  }
 }