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