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