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