1/* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package android.support.v4.view; 18 19import android.support.annotation.Nullable; 20import android.support.test.espresso.Espresso; 21import android.support.test.espresso.IdlingResource; 22import android.support.test.espresso.UiController; 23import android.support.test.espresso.ViewAction; 24import android.support.test.espresso.action.CoordinatesProvider; 25import android.support.test.espresso.action.GeneralClickAction; 26import android.support.test.espresso.action.Press; 27import android.support.test.espresso.action.Tap; 28import android.view.View; 29import android.widget.TextView; 30import org.hamcrest.Matcher; 31 32import static android.support.test.espresso.matcher.ViewMatchers.isAssignableFrom; 33import static android.support.test.espresso.matcher.ViewMatchers.isDisplayingAtLeast; 34 35public class ViewPagerActions { 36 /** 37 * View pager listener that serves as Espresso's {@link IdlingResource} and notifies the 38 * registered callback when the view pager gets to STATE_IDLE state. 39 */ 40 private static class CustomViewPagerListener 41 implements ViewPager.OnPageChangeListener, IdlingResource { 42 private int mCurrState = ViewPager.SCROLL_STATE_IDLE; 43 44 @Nullable 45 private IdlingResource.ResourceCallback mCallback; 46 47 private boolean mNeedsIdle = false; 48 49 @Override 50 public void registerIdleTransitionCallback(ResourceCallback resourceCallback) { 51 mCallback = resourceCallback; 52 } 53 54 @Override 55 public String getName() { 56 return "View pager listener"; 57 } 58 59 @Override 60 public boolean isIdleNow() { 61 if (!mNeedsIdle) { 62 return true; 63 } else { 64 return mCurrState == ViewPager.SCROLL_STATE_IDLE; 65 } 66 } 67 68 @Override 69 public void onPageSelected(int position) { 70 if (mCurrState == ViewPager.SCROLL_STATE_IDLE) { 71 if (mCallback != null) { 72 mCallback.onTransitionToIdle(); 73 } 74 } 75 } 76 77 @Override 78 public void onPageScrollStateChanged(int state) { 79 mCurrState = state; 80 if (mCurrState == ViewPager.SCROLL_STATE_IDLE) { 81 if (mCallback != null) { 82 mCallback.onTransitionToIdle(); 83 } 84 } 85 } 86 87 @Override 88 public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { 89 } 90 } 91 92 private abstract static class WrappedViewAction implements ViewAction { 93 } 94 95 public static ViewAction wrap(final ViewAction baseAction) { 96 if (baseAction instanceof WrappedViewAction) { 97 throw new IllegalArgumentException("Don't wrap an already wrapped action"); 98 } 99 100 return new WrappedViewAction() { 101 @Override 102 public Matcher<View> getConstraints() { 103 return baseAction.getConstraints(); 104 } 105 106 @Override 107 public String getDescription() { 108 return baseAction.getDescription(); 109 } 110 111 @Override 112 public final void perform(UiController uiController, View view) { 113 final ViewPager viewPager = (ViewPager) view; 114 // Add a custom tracker listener 115 final CustomViewPagerListener customListener = new CustomViewPagerListener(); 116 viewPager.addOnPageChangeListener(customListener); 117 118 // Note that we're running the following block in a try-finally construct. This 119 // is needed since some of the wrapped actions are going to throw (expected) 120 // exceptions. If that happens, we still need to clean up after ourselves to 121 // leave the system (Espesso) in a good state. 122 try { 123 // Register our listener as idling resource so that Espresso waits until the 124 // wrapped action results in the view pager getting to the STATE_IDLE state 125 Espresso.registerIdlingResources(customListener); 126 baseAction.perform(uiController, view); 127 customListener.mNeedsIdle = true; 128 uiController.loopMainThreadUntilIdle(); 129 customListener.mNeedsIdle = false; 130 } finally { 131 // Unregister our idling resource 132 Espresso.unregisterIdlingResources(customListener); 133 // And remove our tracker listener from ViewPager 134 viewPager.removeOnPageChangeListener(customListener); 135 } 136 } 137 }; 138 } 139 140 /** 141 * Scrolls <code>ViewPager</code> using arrowScroll method in a specified direction. 142 */ 143 public static ViewAction arrowScroll(final int direction) { 144 return wrap(new ViewAction() { 145 @Override 146 public Matcher<View> getConstraints() { 147 return isDisplayingAtLeast(90); 148 } 149 150 @Override 151 public String getDescription() { 152 return "ViewPager arrow scroll in direction: " + direction; 153 } 154 155 @Override 156 public void perform(UiController uiController, View view) { 157 uiController.loopMainThreadUntilIdle(); 158 159 ViewPager viewPager = (ViewPager) view; 160 viewPager.arrowScroll(direction); 161 uiController.loopMainThreadUntilIdle(); 162 } 163 }); 164 } 165 166 /** 167 * Moves <code>ViewPager</code> to the right by one page. 168 */ 169 public static ViewAction scrollRight(final boolean smoothScroll) { 170 return wrap(new ViewAction() { 171 @Override 172 public Matcher<View> getConstraints() { 173 return isDisplayingAtLeast(90); 174 } 175 176 @Override 177 public String getDescription() { 178 return "ViewPager move one page to the right"; 179 } 180 181 @Override 182 public void perform(UiController uiController, View view) { 183 uiController.loopMainThreadUntilIdle(); 184 185 ViewPager viewPager = (ViewPager) view; 186 int current = viewPager.getCurrentItem(); 187 viewPager.setCurrentItem(current + 1, smoothScroll); 188 189 uiController.loopMainThreadUntilIdle(); 190 } 191 }); 192 } 193 194 /** 195 * Moves <code>ViewPager</code> to the left by one page. 196 */ 197 public static ViewAction scrollLeft(final boolean smoothScroll) { 198 return wrap(new ViewAction() { 199 @Override 200 public Matcher<View> getConstraints() { 201 return isDisplayingAtLeast(90); 202 } 203 204 @Override 205 public String getDescription() { 206 return "ViewPager move one page to the left"; 207 } 208 209 @Override 210 public void perform(UiController uiController, View view) { 211 uiController.loopMainThreadUntilIdle(); 212 213 ViewPager viewPager = (ViewPager) view; 214 int current = viewPager.getCurrentItem(); 215 viewPager.setCurrentItem(current - 1, smoothScroll); 216 217 uiController.loopMainThreadUntilIdle(); 218 } 219 }); 220 } 221 222 /** 223 * Moves <code>ViewPager</code> to the last page. 224 */ 225 public static ViewAction scrollToLast(final boolean smoothScroll) { 226 return wrap(new ViewAction() { 227 @Override 228 public Matcher<View> getConstraints() { 229 return isDisplayingAtLeast(90); 230 } 231 232 @Override 233 public String getDescription() { 234 return "ViewPager move to last page"; 235 } 236 237 @Override 238 public void perform(UiController uiController, View view) { 239 uiController.loopMainThreadUntilIdle(); 240 241 ViewPager viewPager = (ViewPager) view; 242 int size = viewPager.getAdapter().getCount(); 243 if (size > 0) { 244 viewPager.setCurrentItem(size - 1, smoothScroll); 245 } 246 247 uiController.loopMainThreadUntilIdle(); 248 } 249 }); 250 } 251 252 /** 253 * Moves <code>ViewPager</code> to the first page. 254 */ 255 public static ViewAction scrollToFirst(final boolean smoothScroll) { 256 return wrap(new ViewAction() { 257 @Override 258 public Matcher<View> getConstraints() { 259 return isDisplayingAtLeast(90); 260 } 261 262 @Override 263 public String getDescription() { 264 return "ViewPager move to first page"; 265 } 266 267 @Override 268 public void perform(UiController uiController, View view) { 269 uiController.loopMainThreadUntilIdle(); 270 271 ViewPager viewPager = (ViewPager) view; 272 int size = viewPager.getAdapter().getCount(); 273 if (size > 0) { 274 viewPager.setCurrentItem(0, smoothScroll); 275 } 276 277 uiController.loopMainThreadUntilIdle(); 278 } 279 }); 280 } 281 282 /** 283 * Moves <code>ViewPager</code> to specific page. 284 */ 285 public static ViewAction scrollToPage(final int page, final boolean smoothScroll) { 286 return wrap(new ViewAction() { 287 @Override 288 public Matcher<View> getConstraints() { 289 return isDisplayingAtLeast(90); 290 } 291 292 @Override 293 public String getDescription() { 294 return "ViewPager move to page"; 295 } 296 297 @Override 298 public void perform(UiController uiController, View view) { 299 uiController.loopMainThreadUntilIdle(); 300 301 ViewPager viewPager = (ViewPager) view; 302 viewPager.setCurrentItem(page, smoothScroll); 303 304 uiController.loopMainThreadUntilIdle(); 305 } 306 }); 307 } 308 309 /** 310 * Moves <code>ViewPager</code> to specific page. 311 */ 312 public static ViewAction setAdapter(final PagerAdapter adapter) { 313 return new ViewAction() { 314 @Override 315 public Matcher<View> getConstraints() { 316 return isAssignableFrom(ViewPager.class); 317 } 318 319 @Override 320 public String getDescription() { 321 return "ViewPager set adapter"; 322 } 323 324 @Override 325 public void perform(UiController uiController, View view) { 326 uiController.loopMainThreadUntilIdle(); 327 328 ViewPager viewPager = (ViewPager) view; 329 viewPager.setAdapter(adapter); 330 331 uiController.loopMainThreadUntilIdle(); 332 } 333 }; 334 } 335 336 /** 337 * Clicks between two titles in a <code>ViewPager</code> title strip 338 */ 339 public static ViewAction clickBetweenTwoTitles(final String title1, final String title2) { 340 return new GeneralClickAction( 341 Tap.SINGLE, 342 new CoordinatesProvider() { 343 @Override 344 public float[] calculateCoordinates(View view) { 345 PagerTitleStrip pagerStrip = (PagerTitleStrip) view; 346 347 // Get the screen position of the pager strip 348 final int[] viewScreenPosition = new int[2]; 349 pagerStrip.getLocationOnScreen(viewScreenPosition); 350 351 // Get the left / right of the first title 352 int title1Left = 0, title1Right = 0, title2Left = 0, title2Right = 0; 353 final int childCount = pagerStrip.getChildCount(); 354 for (int i = 0; i < childCount; i++) { 355 final View child = pagerStrip.getChildAt(i); 356 if (child instanceof TextView) { 357 final TextView textViewChild = (TextView) child; 358 final CharSequence childText = textViewChild.getText(); 359 if (title1.equals(childText)) { 360 title1Left = textViewChild.getLeft(); 361 title1Right = textViewChild.getRight(); 362 } else if (title2.equals(childText)) { 363 title2Left = textViewChild.getLeft(); 364 title2Right = textViewChild.getRight(); 365 } 366 } 367 } 368 369 if (title1Right < title2Left) { 370 // Title 1 is to the left of title 2 371 return new float[] { 372 viewScreenPosition[0] + (title1Right + title2Left) / 2, 373 viewScreenPosition[1] + pagerStrip.getHeight() / 2 }; 374 } else { 375 // The assumption here is that PagerTitleStrip prevents titles 376 // from overlapping, so if we get here it means that title 1 377 // is to the right of title 2 378 return new float[] { 379 viewScreenPosition[0] + (title2Right + title1Left) / 2, 380 viewScreenPosition[1] + pagerStrip.getHeight() / 2 }; 381 } 382 } 383 }, 384 Press.FINGER); 385 } 386} 387