1/*
2 * Copyright (C) 2016 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.design.widget;
18
19import static android.support.test.InstrumentationRegistry.getInstrumentation;
20import static android.support.test.espresso.Espresso.onView;
21import static android.support.test.espresso.action.ViewActions.swipeUp;
22import static android.support.test.espresso.matcher.ViewMatchers.withId;
23
24import static org.hamcrest.CoreMatchers.is;
25import static org.hamcrest.MatcherAssert.assertThat;
26import static org.junit.Assert.assertFalse;
27import static org.junit.Assert.assertTrue;
28import static org.mockito.Matchers.any;
29import static org.mockito.Matchers.eq;
30import static org.mockito.Matchers.same;
31import static org.mockito.Mockito.atLeastOnce;
32import static org.mockito.Mockito.doCallRealMethod;
33import static org.mockito.Mockito.mock;
34import static org.mockito.Mockito.never;
35import static org.mockito.Mockito.reset;
36import static org.mockito.Mockito.spy;
37import static org.mockito.Mockito.times;
38import static org.mockito.Mockito.verify;
39
40import android.annotation.TargetApi;
41import android.app.Instrumentation;
42import android.content.Context;
43import android.graphics.Rect;
44import android.support.annotation.NonNull;
45import android.support.design.test.R;
46import android.support.design.testutils.CoordinatorLayoutUtils;
47import android.support.design.testutils.CoordinatorLayoutUtils.DependentBehavior;
48import android.support.design.widget.CoordinatorLayout.Behavior;
49import android.support.test.annotation.UiThreadTest;
50import android.support.test.filters.MediumTest;
51import android.support.test.filters.SdkSuppress;
52import android.support.v4.view.GravityCompat;
53import android.support.v4.view.ViewCompat;
54import android.support.v4.view.WindowInsetsCompat;
55import android.view.Gravity;
56import android.view.LayoutInflater;
57import android.view.View;
58import android.view.View.MeasureSpec;
59import android.widget.ImageView;
60
61import org.junit.Before;
62import org.junit.Test;
63
64import java.util.Arrays;
65import java.util.List;
66import java.util.concurrent.atomic.AtomicInteger;
67
68@MediumTest
69public class CoordinatorLayoutTest extends BaseInstrumentationTestCase<CoordinatorLayoutActivity> {
70
71    private Instrumentation mInstrumentation;
72
73    public CoordinatorLayoutTest() {
74        super(CoordinatorLayoutActivity.class);
75    }
76
77    @Before
78    public void setup() {
79        mInstrumentation = getInstrumentation();
80    }
81
82    @Test
83    @SdkSuppress(minSdkVersion = 21)
84    @TargetApi(21)
85    public void testSetFitSystemWindows() throws Throwable {
86        final Instrumentation instrumentation = getInstrumentation();
87        final CoordinatorLayout col = mActivityTestRule.getActivity().mCoordinatorLayout;
88        final View view = new View(col.getContext());
89
90        // Create a mock which calls the default impl of onApplyWindowInsets()
91        final CoordinatorLayout.Behavior<View> mockBehavior =
92                mock(CoordinatorLayout.Behavior.class);
93        doCallRealMethod().when(mockBehavior)
94                .onApplyWindowInsets(same(col), same(view), any(WindowInsetsCompat.class));
95
96        // Assert that the CoL is currently not set to fitSystemWindows
97        assertFalse(col.getFitsSystemWindows());
98
99        // Now add a view with our mocked behavior to the CoordinatorLayout
100        view.setFitsSystemWindows(true);
101        mActivityTestRule.runOnUiThread(new Runnable() {
102            @Override
103            public void run() {
104                final CoordinatorLayout.LayoutParams lp = col.generateDefaultLayoutParams();
105                lp.setBehavior(mockBehavior);
106                col.addView(view, lp);
107            }
108        });
109        instrumentation.waitForIdleSync();
110
111        // Now request some insets and wait for the pass to happen
112        mActivityTestRule.runOnUiThread(new Runnable() {
113            @Override
114            public void run() {
115                col.requestApplyInsets();
116            }
117        });
118        instrumentation.waitForIdleSync();
119
120        // Verify that onApplyWindowInsets() has not been called
121        verify(mockBehavior, never())
122                .onApplyWindowInsets(same(col), same(view), any(WindowInsetsCompat.class));
123
124        // Now enable fits system windows and wait for a pass to happen
125        mActivityTestRule.runOnUiThread(new Runnable() {
126            @Override
127            public void run() {
128                col.setFitsSystemWindows(true);
129            }
130        });
131        instrumentation.waitForIdleSync();
132
133        // Verify that onApplyWindowInsets() has been called with some insets
134        verify(mockBehavior, atLeastOnce())
135                .onApplyWindowInsets(same(col), same(view), any(WindowInsetsCompat.class));
136    }
137
138    @Test
139    public void testLayoutChildren() throws Throwable {
140        final Instrumentation instrumentation = getInstrumentation();
141        final CoordinatorLayout col = mActivityTestRule.getActivity().mCoordinatorLayout;
142        final View view = new View(col.getContext());
143        mActivityTestRule.runOnUiThread(new Runnable() {
144            @Override
145            public void run() {
146                col.addView(view, 100, 100);
147            }
148        });
149        instrumentation.waitForIdleSync();
150        int horizontallyCentered = (col.getWidth() - view.getWidth()) / 2;
151        int end = col.getWidth() - view.getWidth();
152        int verticallyCentered = (col.getHeight() - view.getHeight()) / 2;
153        int bottom = col.getHeight() - view.getHeight();
154        final int[][] testCases = {
155                // gravity, expected left, expected top
156                {Gravity.NO_GRAVITY, 0, 0},
157                {Gravity.LEFT, 0, 0},
158                {GravityCompat.START, 0, 0},
159                {Gravity.TOP, 0, 0},
160                {Gravity.CENTER, horizontallyCentered, verticallyCentered},
161                {Gravity.CENTER_HORIZONTAL, horizontallyCentered, 0},
162                {Gravity.CENTER_VERTICAL, 0, verticallyCentered},
163                {Gravity.RIGHT, end, 0},
164                {GravityCompat.END, end, 0},
165                {Gravity.BOTTOM, 0, bottom},
166                {Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM, horizontallyCentered, bottom},
167                {Gravity.RIGHT | Gravity.CENTER_VERTICAL, end, verticallyCentered},
168        };
169        for (final int[] testCase : testCases) {
170            mActivityTestRule.runOnUiThread(new Runnable() {
171                @Override
172                public void run() {
173                    final CoordinatorLayout.LayoutParams lp =
174                            (CoordinatorLayout.LayoutParams) view.getLayoutParams();
175                    lp.gravity = testCase[0];
176                    view.setLayoutParams(lp);
177                }
178            });
179            instrumentation.waitForIdleSync();
180            mActivityTestRule.runOnUiThread(new Runnable() {
181                @Override
182                public void run() {
183                    assertThat("Gravity: " + testCase[0], view.getLeft(), is(testCase[1]));
184                    assertThat("Gravity: " + testCase[0], view.getTop(), is(testCase[2]));
185                }
186            });
187        }
188    }
189
190    @Test
191    public void testInsetDependency() {
192        final CoordinatorLayout col = mActivityTestRule.getActivity().mCoordinatorLayout;
193
194        final CoordinatorLayout.LayoutParams lpInsetLeft = col.generateDefaultLayoutParams();
195        lpInsetLeft.insetEdge = Gravity.LEFT;
196
197        final CoordinatorLayout.LayoutParams lpInsetRight = col.generateDefaultLayoutParams();
198        lpInsetRight.insetEdge = Gravity.RIGHT;
199
200        final CoordinatorLayout.LayoutParams lpInsetTop = col.generateDefaultLayoutParams();
201        lpInsetTop.insetEdge = Gravity.TOP;
202
203        final CoordinatorLayout.LayoutParams lpInsetBottom = col.generateDefaultLayoutParams();
204        lpInsetBottom.insetEdge = Gravity.BOTTOM;
205
206        final CoordinatorLayout.LayoutParams lpDodgeLeft = col.generateDefaultLayoutParams();
207        lpDodgeLeft.dodgeInsetEdges = Gravity.LEFT;
208
209        final CoordinatorLayout.LayoutParams lpDodgeLeftAndTop = col.generateDefaultLayoutParams();
210        lpDodgeLeftAndTop.dodgeInsetEdges = Gravity.LEFT | Gravity.TOP;
211
212        final CoordinatorLayout.LayoutParams lpDodgeAll = col.generateDefaultLayoutParams();
213        lpDodgeAll.dodgeInsetEdges = Gravity.FILL;
214
215        final View a = new View(col.getContext());
216        final View b = new View(col.getContext());
217
218        assertThat(dependsOn(lpDodgeLeft, lpInsetLeft, col, a, b), is(true));
219        assertThat(dependsOn(lpDodgeLeft, lpInsetRight, col, a, b), is(false));
220        assertThat(dependsOn(lpDodgeLeft, lpInsetTop, col, a, b), is(false));
221        assertThat(dependsOn(lpDodgeLeft, lpInsetBottom, col, a, b), is(false));
222
223        assertThat(dependsOn(lpDodgeLeftAndTop, lpInsetLeft, col, a, b), is(true));
224        assertThat(dependsOn(lpDodgeLeftAndTop, lpInsetRight, col, a, b), is(false));
225        assertThat(dependsOn(lpDodgeLeftAndTop, lpInsetTop, col, a, b), is(true));
226        assertThat(dependsOn(lpDodgeLeftAndTop, lpInsetBottom, col, a, b), is(false));
227
228        assertThat(dependsOn(lpDodgeAll, lpInsetLeft, col, a, b), is(true));
229        assertThat(dependsOn(lpDodgeAll, lpInsetRight, col, a, b), is(true));
230        assertThat(dependsOn(lpDodgeAll, lpInsetTop, col, a, b), is(true));
231        assertThat(dependsOn(lpDodgeAll, lpInsetBottom, col, a, b), is(true));
232
233        assertThat(dependsOn(lpInsetLeft, lpDodgeLeft, col, a, b), is(false));
234    }
235
236    private static boolean dependsOn(CoordinatorLayout.LayoutParams lpChild,
237            CoordinatorLayout.LayoutParams lpDependency, CoordinatorLayout col,
238            View child, View dependency) {
239        child.setLayoutParams(lpChild);
240        dependency.setLayoutParams(lpDependency);
241        return lpChild.dependsOn(col, child, dependency);
242    }
243
244    @Test
245    public void testInsetEdge() throws Throwable {
246        final Instrumentation instrumentation = getInstrumentation();
247        final CoordinatorLayout col = mActivityTestRule.getActivity().mCoordinatorLayout;
248
249        final View insetView = new View(col.getContext());
250        final View dodgeInsetView = new View(col.getContext());
251        final AtomicInteger originalTop = new AtomicInteger();
252
253        mActivityTestRule.runOnUiThread(new Runnable() {
254            @Override
255            public void run() {
256                CoordinatorLayout.LayoutParams lpInsetView = col.generateDefaultLayoutParams();
257                lpInsetView.width = CoordinatorLayout.LayoutParams.MATCH_PARENT;
258                lpInsetView.height = 100;
259                lpInsetView.gravity = Gravity.TOP | Gravity.LEFT;
260                lpInsetView.insetEdge = Gravity.TOP;
261                col.addView(insetView, lpInsetView);
262                insetView.setBackgroundColor(0xFF0000FF);
263
264                CoordinatorLayout.LayoutParams lpDodgeInsetView = col.generateDefaultLayoutParams();
265                lpDodgeInsetView.width = 100;
266                lpDodgeInsetView.height = 100;
267                lpDodgeInsetView.gravity = Gravity.TOP | Gravity.LEFT;
268                lpDodgeInsetView.dodgeInsetEdges = Gravity.TOP;
269                col.addView(dodgeInsetView, lpDodgeInsetView);
270                dodgeInsetView.setBackgroundColor(0xFFFF0000);
271            }
272        });
273        instrumentation.waitForIdleSync();
274        mActivityTestRule.runOnUiThread(new Runnable() {
275            @Override
276            public void run() {
277                List<View> dependencies = col.getDependencies(dodgeInsetView);
278                assertThat(dependencies.size(), is(1));
279                assertThat(dependencies.get(0), is(insetView));
280
281                // Move the insetting view
282                originalTop.set(dodgeInsetView.getTop());
283                assertThat(originalTop.get(), is(insetView.getBottom()));
284                ViewCompat.offsetTopAndBottom(insetView, 123);
285            }
286        });
287        instrumentation.waitForIdleSync();
288        mActivityTestRule.runOnUiThread(new Runnable() {
289            @Override
290            public void run() {
291                // Confirm that the dodging view was moved by the same size
292                assertThat(dodgeInsetView.getTop() - originalTop.get(), is(123));
293            }
294        });
295    }
296
297    @Test
298    public void testDependentViewChanged() throws Throwable {
299        final Instrumentation instrumentation = getInstrumentation();
300        final CoordinatorLayout col = mActivityTestRule.getActivity().mCoordinatorLayout;
301
302        // Add two views, A & B, where B depends on A
303        final View viewA = new View(col.getContext());
304        final CoordinatorLayout.LayoutParams lpA = col.generateDefaultLayoutParams();
305        lpA.width = 100;
306        lpA.height = 100;
307
308        final View viewB = new View(col.getContext());
309        final CoordinatorLayout.LayoutParams lpB = col.generateDefaultLayoutParams();
310        lpB.width = 100;
311        lpB.height = 100;
312        final CoordinatorLayout.Behavior behavior =
313                spy(new DependentBehavior(viewA));
314        lpB.setBehavior(behavior);
315
316        mActivityTestRule.runOnUiThread(new Runnable() {
317            @Override
318            public void run() {
319                col.addView(viewA, lpA);
320                col.addView(viewB, lpB);
321            }
322        });
323        instrumentation.waitForIdleSync();
324
325        // Reset the Behavior since onDependentViewChanged may have already been called as part of
326        // any layout/draw passes already
327        reset(behavior);
328
329        // Now offset view A
330        mActivityTestRule.runOnUiThread(new Runnable() {
331            @Override
332            public void run() {
333                ViewCompat.offsetLeftAndRight(viewA, 20);
334                ViewCompat.offsetTopAndBottom(viewA, 20);
335            }
336        });
337        instrumentation.waitForIdleSync();
338
339        // And assert that view B's Behavior was called appropriately
340        verify(behavior, times(1)).onDependentViewChanged(col, viewB, viewA);
341    }
342
343    @Test
344    public void testDependentViewRemoved() throws Throwable {
345        final Instrumentation instrumentation = getInstrumentation();
346        final CoordinatorLayout col = mActivityTestRule.getActivity().mCoordinatorLayout;
347
348        // Add two views, A & B, where B depends on A
349        final View viewA = new View(col.getContext());
350        final View viewB = new View(col.getContext());
351        final CoordinatorLayout.LayoutParams lpB = col.generateDefaultLayoutParams();
352        final CoordinatorLayout.Behavior behavior =
353                spy(new DependentBehavior(viewA));
354        lpB.setBehavior(behavior);
355
356        mActivityTestRule.runOnUiThread(new Runnable() {
357            @Override
358            public void run() {
359                col.addView(viewA);
360                col.addView(viewB, lpB);
361            }
362        });
363        instrumentation.waitForIdleSync();
364
365        // Now remove view A
366        mActivityTestRule.runOnUiThread(new Runnable() {
367            @Override
368            public void run() {
369                col.removeView(viewA);
370            }
371        });
372
373        // And assert that View B's Behavior was called appropriately
374        verify(behavior, times(1)).onDependentViewRemoved(col, viewB, viewA);
375    }
376
377    @Test
378    public void testGetDependenciesAfterDependentViewRemoved() throws Throwable {
379        final Instrumentation instrumentation = getInstrumentation();
380        final CoordinatorLayout col = mActivityTestRule.getActivity().mCoordinatorLayout;
381
382        // Add two views, A & B, where B depends on A
383        final View viewA = new View(col.getContext());
384        final View viewB = new View(col.getContext());
385        final CoordinatorLayout.LayoutParams lpB = col.generateDefaultLayoutParams();
386        final CoordinatorLayout.Behavior behavior
387                = new CoordinatorLayoutUtils.DependentBehavior(viewA) {
388            @Override
389            public void onDependentViewRemoved(CoordinatorLayout parent, View child,
390                    View dependency) {
391                parent.getDependencies(child);
392            }
393        };
394        lpB.setBehavior(behavior);
395
396        // Now add views
397        mActivityTestRule.runOnUiThread(new Runnable() {
398            @Override
399            public void run() {
400                col.addView(viewA);
401                col.addView(viewB, lpB);
402            }
403        });
404
405        // Wait for a layout
406        instrumentation.waitForIdleSync();
407
408        // Now remove view A, which will trigger onDependentViewRemoved() on view B's behavior
409        mActivityTestRule.runOnUiThread(new Runnable() {
410            @Override
411            public void run() {
412                col.removeView(viewA);
413            }
414        });
415    }
416
417    @Test
418    public void testDodgeInsetBeforeLayout() throws Throwable {
419        final CoordinatorLayout col = mActivityTestRule.getActivity().mCoordinatorLayout;
420
421        // Add a dummy view, which will be used to trigger a hierarchy change.
422        final View dummy = new View(col.getContext());
423
424        mActivityTestRule.runOnUiThread(new Runnable() {
425            @Override
426            public void run() {
427                col.addView(dummy);
428            }
429        });
430
431        // Wait for a layout.
432        mInstrumentation.waitForIdleSync();
433
434        final View dodge = new View(col.getContext());
435        final CoordinatorLayout.LayoutParams lpDodge = col.generateDefaultLayoutParams();
436        lpDodge.dodgeInsetEdges = Gravity.BOTTOM;
437        lpDodge.setBehavior(new Behavior() {
438            @Override
439            public boolean getInsetDodgeRect(CoordinatorLayout parent, View child, Rect rect) {
440                // Any non-empty rect is fine here.
441                rect.set(0, 0, 10, 10);
442                return true;
443            }
444        });
445
446        mActivityTestRule.runOnUiThread(new Runnable() {
447            @Override
448            public void run() {
449                col.addView(dodge, lpDodge);
450
451                // Ensure the new view is in the list of children.
452                int heightSpec = MeasureSpec.makeMeasureSpec(col.getHeight(), MeasureSpec.EXACTLY);
453                int widthSpec = MeasureSpec.makeMeasureSpec(col.getWidth(), MeasureSpec.EXACTLY);
454                col.measure(widthSpec, heightSpec);
455
456                // Force a hierarchy change.
457                col.removeView(dummy);
458            }
459        });
460
461        // Wait for a layout.
462        mInstrumentation.waitForIdleSync();
463    }
464
465    @Test
466    public void testGoneViewsNotMeasuredLaidOut() throws Throwable {
467        final CoordinatorLayoutActivity activity = mActivityTestRule.getActivity();
468        final CoordinatorLayout col = activity.mCoordinatorLayout;
469
470        // Now create a GONE view and add it to the CoordinatorLayout
471        final View imageView = new View(activity);
472        imageView.setVisibility(View.GONE);
473        mActivityTestRule.runOnUiThread(new Runnable() {
474            @Override
475            public void run() {
476                col.addView(imageView, 200, 200);
477            }
478        });
479        // Wait for a layout and measure pass
480        mInstrumentation.waitForIdleSync();
481
482        // And assert that it has not been laid out
483        assertFalse(imageView.getMeasuredWidth() > 0);
484        assertFalse(imageView.getMeasuredHeight() > 0);
485        assertFalse(ViewCompat.isLaidOut(imageView));
486
487        // Now set the view to INVISIBLE
488        mActivityTestRule.runOnUiThread(new Runnable() {
489            @Override
490            public void run() {
491                imageView.setVisibility(View.INVISIBLE);
492            }
493        });
494        // Wait for a layout and measure pass
495        mInstrumentation.waitForIdleSync();
496
497        // And assert that it has been laid out
498        assertTrue(imageView.getMeasuredWidth() > 0);
499        assertTrue(imageView.getMeasuredHeight() > 0);
500        assertTrue(ViewCompat.isLaidOut(imageView));
501    }
502
503    @Test
504    public void testNestedScrollingDispatchesToBehavior() throws Throwable {
505        final CoordinatorLayoutActivity activity = mActivityTestRule.getActivity();
506        final CoordinatorLayout col = activity.mCoordinatorLayout;
507
508        // Now create a view and add it to the CoordinatorLayout with the spy behavior,
509        // along with a NestedScrollView
510        final ImageView imageView = new ImageView(activity);
511        final CoordinatorLayout.Behavior behavior = spy(new NestedScrollingBehavior());
512        mActivityTestRule.runOnUiThread(new Runnable() {
513            @Override
514            public void run() {
515                LayoutInflater.from(activity).inflate(R.layout.include_nestedscrollview, col, true);
516
517                CoordinatorLayout.LayoutParams clp = new CoordinatorLayout.LayoutParams(200, 200);
518                clp.setBehavior(behavior);
519                col.addView(imageView, clp);
520            }
521        });
522
523        // Now vertically swipe up on the NSV, causing nested scrolling to occur
524        onView(withId(R.id.nested_scrollview)).perform(swipeUp());
525
526        // Verify that the Behavior's onStartNestedScroll was called once
527        verify(behavior, times(1)).onStartNestedScroll(
528                eq(col), // parent
529                eq(imageView), // child
530                any(View.class), // target
531                any(View.class), // direct child target
532                any(int.class)); // axes
533
534        // Verify that the Behavior's onNestedScrollAccepted was called once
535        verify(behavior, times(1)).onNestedScrollAccepted(
536                eq(col), // parent
537                eq(imageView), // child
538                any(View.class), // target
539                any(View.class), // direct child target
540                any(int.class)); // axes
541
542        // Verify that the Behavior's onNestedPreScroll was called at least once
543        verify(behavior, atLeastOnce()).onNestedPreScroll(
544                eq(col), // parent
545                eq(imageView), // child
546                any(View.class), // target
547                any(int.class), // dx
548                any(int.class), // dy
549                any(int[].class)); // consumed
550
551        // Verify that the Behavior's onNestedScroll was called at least once
552        verify(behavior, atLeastOnce()).onNestedScroll(
553                eq(col), // parent
554                eq(imageView), // child
555                any(View.class), // target
556                any(int.class), // dx consumed
557                any(int.class), // dy consumed
558                any(int.class), // dx unconsumed
559                any(int.class)); // dy unconsumed
560
561        // Verify that the Behavior's onStopNestedScroll was called once
562        verify(behavior, times(1)).onStopNestedScroll(
563                eq(col), // parent
564                eq(imageView), // child
565                any(View.class)); // target
566    }
567
568    @Test
569    public void testNestedScrollingDispatchingToBehaviorWithGoneView() throws Throwable {
570        final CoordinatorLayoutActivity activity = mActivityTestRule.getActivity();
571        final CoordinatorLayout col = activity.mCoordinatorLayout;
572
573        // Now create a GONE view and add it to the CoordinatorLayout with the spy behavior,
574        // along with a NestedScrollView
575        final ImageView imageView = new ImageView(activity);
576        imageView.setVisibility(View.GONE);
577        final CoordinatorLayout.Behavior behavior = spy(new NestedScrollingBehavior());
578        mActivityTestRule.runOnUiThread(new Runnable() {
579            @Override
580            public void run() {
581                LayoutInflater.from(activity).inflate(R.layout.include_nestedscrollview, col, true);
582
583                CoordinatorLayout.LayoutParams clp = new CoordinatorLayout.LayoutParams(200, 200);
584                clp.setBehavior(behavior);
585                col.addView(imageView, clp);
586            }
587        });
588
589        // Now vertically swipe up on the NSV, causing nested scrolling to occur
590        onView(withId(R.id.nested_scrollview)).perform(swipeUp());
591
592        // Verify that the Behavior's onStartNestedScroll was not called
593        verify(behavior, never()).onStartNestedScroll(
594                eq(col), // parent
595                eq(imageView), // child
596                any(View.class), // target
597                any(View.class), // direct child target
598                any(int.class)); // axes
599
600        // Verify that the Behavior's onNestedScrollAccepted was not called
601        verify(behavior, never()).onNestedScrollAccepted(
602                eq(col), // parent
603                eq(imageView), // child
604                any(View.class), // target
605                any(View.class), // direct child target
606                any(int.class)); // axes
607
608        // Verify that the Behavior's onNestedPreScroll was not called
609        verify(behavior, never()).onNestedPreScroll(
610                eq(col), // parent
611                eq(imageView), // child
612                any(View.class), // target
613                any(int.class), // dx
614                any(int.class), // dy
615                any(int[].class)); // consumed
616
617        // Verify that the Behavior's onNestedScroll was not called
618        verify(behavior, never()).onNestedScroll(
619                eq(col), // parent
620                eq(imageView), // child
621                any(View.class), // target
622                any(int.class), // dx consumed
623                any(int.class), // dy consumed
624                any(int.class), // dx unconsumed
625                any(int.class)); // dy unconsumed
626
627        // Verify that the Behavior's onStopNestedScroll was not called
628        verify(behavior, never()).onStopNestedScroll(
629                eq(col), // parent
630                eq(imageView), // child
631                any(View.class)); // target
632    }
633
634    @Test
635    public void testNestedScrollingTriggeringDependentViewChanged() throws Throwable {
636        final CoordinatorLayoutActivity activity = mActivityTestRule.getActivity();
637        final CoordinatorLayout col = activity.mCoordinatorLayout;
638
639        // First a NestedScrollView to trigger nested scrolling
640        final View scrollView = LayoutInflater.from(activity).inflate(
641                R.layout.include_nestedscrollview, col, false);
642
643        // Now create a View and Behavior which depend on the scrollview
644        final ImageView dependentView = new ImageView(activity);
645        final CoordinatorLayout.Behavior dependentBehavior = spy(new DependentBehavior(scrollView));
646
647        // Finally a view which accepts nested scrolling in the CoordinatorLayout
648        final ImageView nestedScrollAwareView = new ImageView(activity);
649
650        mActivityTestRule.runOnUiThread(new Runnable() {
651            @Override
652            public void run() {
653                // First add the ScrollView
654                col.addView(scrollView);
655
656                // Now add the view which depends on the scrollview
657                CoordinatorLayout.LayoutParams clp = new CoordinatorLayout.LayoutParams(200, 200);
658                clp.setBehavior(dependentBehavior);
659                col.addView(dependentView, clp);
660
661                // Now add the nested scrolling aware view
662                clp = new CoordinatorLayout.LayoutParams(200, 200);
663                clp.setBehavior(new NestedScrollingBehavior());
664                col.addView(nestedScrollAwareView, clp);
665            }
666        });
667
668        // Wait for any layouts, and reset the Behavior so that the call counts are 0
669        getInstrumentation().waitForIdleSync();
670        reset(dependentBehavior);
671
672        // Now vertically swipe up on the NSV, causing nested scrolling to occur
673        onView(withId(R.id.nested_scrollview)).perform(swipeUp());
674
675        // Verify that the Behavior's onDependentViewChanged is not called due to the
676        // nested scroll
677        verify(dependentBehavior, never()).onDependentViewChanged(
678                eq(col), // parent
679                eq(dependentView), // child
680                eq(scrollView)); // axes
681    }
682
683    @Test
684    public void testDodgeInsetViewWithEmptyBounds() throws Throwable {
685        final CoordinatorLayout col = mActivityTestRule.getActivity().mCoordinatorLayout;
686
687        // Add a view with zero height/width which is set to dodge its bounds
688        final View view = new View(col.getContext());
689        final Behavior spyBehavior = spy(new DodgeBoundsBehavior());
690        mActivityTestRule.runOnUiThread(new Runnable() {
691            @Override
692            public void run() {
693                final CoordinatorLayout.LayoutParams lp = col.generateDefaultLayoutParams();
694                lp.dodgeInsetEdges = Gravity.BOTTOM;
695                lp.gravity = Gravity.BOTTOM;
696                lp.height = 0;
697                lp.width = 0;
698                lp.setBehavior(spyBehavior);
699                col.addView(view, lp);
700            }
701        });
702
703        // Wait for a layout
704        mInstrumentation.waitForIdleSync();
705
706        // Now add an non-empty bounds inset view to the bottom of the CoordinatorLayout
707        mActivityTestRule.runOnUiThread(new Runnable() {
708            @Override
709            public void run() {
710                final View dodge = new View(col.getContext());
711                final CoordinatorLayout.LayoutParams lp = col.generateDefaultLayoutParams();
712                lp.insetEdge = Gravity.BOTTOM;
713                lp.gravity = Gravity.BOTTOM;
714                lp.height = 60;
715                lp.width = CoordinatorLayout.LayoutParams.MATCH_PARENT;
716                col.addView(dodge, lp);
717            }
718        });
719
720        // Verify that the Behavior of the view with empty bounds does not have its
721        // getInsetDodgeRect() called
722        verify(spyBehavior, never())
723                .getInsetDodgeRect(same(col), same(view), any(Rect.class));
724    }
725
726    public static class NestedScrollingBehavior extends CoordinatorLayout.Behavior<View> {
727        @Override
728        public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child,
729                View directTargetChild, View target, int nestedScrollAxes) {
730            // Return true so that we always accept nested scroll events
731            return true;
732        }
733    }
734
735    public static class DodgeBoundsBehavior extends Behavior<View> {
736        @Override
737        public boolean getInsetDodgeRect(CoordinatorLayout parent, View child, Rect rect) {
738            rect.set(child.getLeft(), child.getTop(), child.getRight(), child.getBottom());
739            return true;
740        }
741    }
742
743    @UiThreadTest
744    @Test
745    public void testAnchorDependencyGraph() throws Throwable {
746        final CoordinatorLayout col = mActivityTestRule.getActivity().mCoordinatorLayout;
747
748        // Override hashcode because of implementation of SimpleArrayMap used in
749        // DirectedAcyclicGraph used for sorting dependencies. Hashcode of anchored view has to be
750        // greater than of the one it is anchored to in order to reproduce the error.
751        final View anchor = createViewWithHashCode(col.getContext(), 2);
752        anchor.setId(R.id.anchor);
753
754        final View ship = createViewWithHashCode(col.getContext(), 3);
755        final CoordinatorLayout.LayoutParams lp = col.generateDefaultLayoutParams();
756        lp.setAnchorId(R.id.anchor);
757
758        col.addView(anchor);
759        col.addView(ship, lp);
760
761        // Get dependencies immediately to avoid possible call to onMeasure(), since error
762        // only happens on first computing of sorted dependencies.
763        List<View> dependencySortedChildren = col.getDependencySortedChildren();
764        assertThat(dependencySortedChildren, is(Arrays.asList(anchor, ship)));
765    }
766
767    @NonNull
768    private View createViewWithHashCode(final Context context, final int hashCode) {
769        return new View(context) {
770            @Override
771            public int hashCode() {
772                return hashCode;
773            }
774        };
775    }
776}
777