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