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 * Moves <code>ViewPager</code> to the right by one page. 142 */ 143 public static ViewAction scrollRight(final boolean smoothScroll) { 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 move one page to the right"; 153 } 154 155 @Override 156 public void perform(UiController uiController, View view) { 157 uiController.loopMainThreadUntilIdle(); 158 159 ViewPager viewPager = (ViewPager) view; 160 int current = viewPager.getCurrentItem(); 161 viewPager.setCurrentItem(current + 1, smoothScroll); 162 163 uiController.loopMainThreadUntilIdle(); 164 } 165 }); 166 } 167 168 /** 169 * Moves <code>ViewPager</code> to the left by one page. 170 */ 171 public static ViewAction scrollLeft(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 left"; 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 last page. 198 */ 199 public static ViewAction scrollToLast(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 to last page"; 209 } 210 211 @Override 212 public void perform(UiController uiController, View view) { 213 uiController.loopMainThreadUntilIdle(); 214 215 ViewPager viewPager = (ViewPager) view; 216 int size = viewPager.getAdapter().getCount(); 217 if (size > 0) { 218 viewPager.setCurrentItem(size - 1, smoothScroll); 219 } 220 221 uiController.loopMainThreadUntilIdle(); 222 } 223 }); 224 } 225 226 /** 227 * Moves <code>ViewPager</code> to the first page. 228 */ 229 public static ViewAction scrollToFirst(final boolean smoothScroll) { 230 return wrap(new ViewAction() { 231 @Override 232 public Matcher<View> getConstraints() { 233 return isDisplayingAtLeast(90); 234 } 235 236 @Override 237 public String getDescription() { 238 return "ViewPager move to first page"; 239 } 240 241 @Override 242 public void perform(UiController uiController, View view) { 243 uiController.loopMainThreadUntilIdle(); 244 245 ViewPager viewPager = (ViewPager) view; 246 int size = viewPager.getAdapter().getCount(); 247 if (size > 0) { 248 viewPager.setCurrentItem(0, smoothScroll); 249 } 250 251 uiController.loopMainThreadUntilIdle(); 252 } 253 }); 254 } 255 256 /** 257 * Moves <code>ViewPager</code> to specific page. 258 */ 259 public static ViewAction scrollToPage(final int page, final boolean smoothScroll) { 260 return wrap(new ViewAction() { 261 @Override 262 public Matcher<View> getConstraints() { 263 return isDisplayingAtLeast(90); 264 } 265 266 @Override 267 public String getDescription() { 268 return "ViewPager move to page"; 269 } 270 271 @Override 272 public void perform(UiController uiController, View view) { 273 uiController.loopMainThreadUntilIdle(); 274 275 ViewPager viewPager = (ViewPager) view; 276 viewPager.setCurrentItem(page, smoothScroll); 277 278 uiController.loopMainThreadUntilIdle(); 279 } 280 }); 281 } 282 283 /** 284 * Moves <code>ViewPager</code> to specific page. 285 */ 286 public static ViewAction setAdapter(final PagerAdapter adapter) { 287 return new ViewAction() { 288 @Override 289 public Matcher<View> getConstraints() { 290 return isAssignableFrom(ViewPager.class); 291 } 292 293 @Override 294 public String getDescription() { 295 return "ViewPager set adapter"; 296 } 297 298 @Override 299 public void perform(UiController uiController, View view) { 300 uiController.loopMainThreadUntilIdle(); 301 302 ViewPager viewPager = (ViewPager) view; 303 viewPager.setAdapter(adapter); 304 305 uiController.loopMainThreadUntilIdle(); 306 } 307 }; 308 } 309 310 /** 311 * Clicks between two titles in a <code>ViewPager</code> title strip 312 */ 313 public static ViewAction clickBetweenTwoTitles(final String title1, final String title2) { 314 return new GeneralClickAction( 315 Tap.SINGLE, 316 new CoordinatesProvider() { 317 @Override 318 public float[] calculateCoordinates(View view) { 319 PagerTitleStrip pagerStrip = (PagerTitleStrip) view; 320 321 // Get the screen position of the pager strip 322 final int[] viewScreenPosition = new int[2]; 323 pagerStrip.getLocationOnScreen(viewScreenPosition); 324 325 // Get the left / right of the first title 326 int title1Left = 0, title1Right = 0, title2Left = 0, title2Right = 0; 327 final int childCount = pagerStrip.getChildCount(); 328 for (int i = 0; i < childCount; i++) { 329 final View child = pagerStrip.getChildAt(i); 330 if (child instanceof TextView) { 331 final TextView textViewChild = (TextView) child; 332 final CharSequence childText = textViewChild.getText(); 333 if (title1.equals(childText)) { 334 title1Left = textViewChild.getLeft(); 335 title1Right = textViewChild.getRight(); 336 } else if (title2.equals(childText)) { 337 title2Left = textViewChild.getLeft(); 338 title2Right = textViewChild.getRight(); 339 } 340 } 341 } 342 343 if (title1Right < title2Left) { 344 // Title 1 is to the left of title 2 345 return new float[] { 346 viewScreenPosition[0] + (title1Right + title2Left) / 2, 347 viewScreenPosition[1] + pagerStrip.getHeight() / 2 }; 348 } else { 349 // The assumption here is that PagerTitleStrip prevents titles 350 // from overlapping, so if we get here it means that title 1 351 // is to the right of title 2 352 return new float[] { 353 viewScreenPosition[0] + (title2Right + title1Left) / 2, 354 viewScreenPosition[1] + pagerStrip.getHeight() / 2 }; 355 } 356 } 357 }, 358 Press.FINGER); 359 } 360} 361