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