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