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