blob: 578f8d5b09a928d9bc6537d194b53d6f3dd7e79c [file] [log] [blame]
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.coordinatorlayout.widget;
import static android.support.test.InstrumentationRegistry.getInstrumentation;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.action.ViewActions.swipeUp;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Matchers.same;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doCallRealMethod;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import android.app.Instrumentation;
import android.content.Context;
import android.graphics.Rect;
import android.support.test.annotation.UiThreadTest;
import android.support.test.filters.MediumTest;
import android.support.test.filters.SdkSuppress;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.MeasureSpec;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.coordinatorlayout.test.R;
import androidx.coordinatorlayout.testutils.CoordinatorLayoutUtils;
import androidx.coordinatorlayout.testutils.CoordinatorLayoutUtils.DependentBehavior;
import androidx.core.view.GravityCompat;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
@MediumTest
@RunWith(AndroidJUnit4.class)
public class CoordinatorLayoutTest {
@Rule
public final ActivityTestRule<CoordinatorLayoutActivity> mActivityTestRule;
private Instrumentation mInstrumentation;
public CoordinatorLayoutTest() {
mActivityTestRule = new ActivityTestRule<>(CoordinatorLayoutActivity.class);
}
@Before
public void setup() {
mInstrumentation = getInstrumentation();
}
@Test
@SdkSuppress(minSdkVersion = 21)
public void testSetFitSystemWindows() throws Throwable {
final Instrumentation instrumentation = getInstrumentation();
final CoordinatorLayout col = mActivityTestRule.getActivity().mCoordinatorLayout;
final View view = new View(col.getContext());
// Create a mock which calls the default impl of onApplyWindowInsets()
final CoordinatorLayout.Behavior<View> mockBehavior =
mock(CoordinatorLayout.Behavior.class);
doCallRealMethod().when(mockBehavior)
.onApplyWindowInsets(same(col), same(view), any(WindowInsetsCompat.class));
// Assert that the CoL is currently not set to fitSystemWindows
assertFalse(col.getFitsSystemWindows());
// Now add a view with our mocked behavior to the CoordinatorLayout
view.setFitsSystemWindows(true);
mActivityTestRule.runOnUiThread(new Runnable() {
@Override
public void run() {
final CoordinatorLayout.LayoutParams lp = col.generateDefaultLayoutParams();
lp.setBehavior(mockBehavior);
col.addView(view, lp);
}
});
instrumentation.waitForIdleSync();
// Now request some insets and wait for the pass to happen
mActivityTestRule.runOnUiThread(new Runnable() {
@Override
public void run() {
col.requestApplyInsets();
}
});
instrumentation.waitForIdleSync();
// Verify that onApplyWindowInsets() has not been called
verify(mockBehavior, never())
.onApplyWindowInsets(same(col), same(view), any(WindowInsetsCompat.class));
// Now enable fits system windows and wait for a pass to happen
mActivityTestRule.runOnUiThread(new Runnable() {
@Override
public void run() {
col.setFitsSystemWindows(true);
}
});
instrumentation.waitForIdleSync();
// Verify that onApplyWindowInsets() has been called with some insets
verify(mockBehavior, atLeastOnce())
.onApplyWindowInsets(same(col), same(view), any(WindowInsetsCompat.class));
}
@Test
public void testLayoutChildren() throws Throwable {
final Instrumentation instrumentation = getInstrumentation();
final CoordinatorLayout col = mActivityTestRule.getActivity().mCoordinatorLayout;
final View view = new View(col.getContext());
mActivityTestRule.runOnUiThread(new Runnable() {
@Override
public void run() {
col.addView(view, 100, 100);
}
});
instrumentation.waitForIdleSync();
int horizontallyCentered = (col.getWidth() - view.getWidth()) / 2;
int end = col.getWidth() - view.getWidth();
int verticallyCentered = (col.getHeight() - view.getHeight()) / 2;
int bottom = col.getHeight() - view.getHeight();
final int[][] testCases = {
// gravity, expected left, expected top
{Gravity.NO_GRAVITY, 0, 0},
{Gravity.LEFT, 0, 0},
{GravityCompat.START, 0, 0},
{Gravity.TOP, 0, 0},
{Gravity.CENTER, horizontallyCentered, verticallyCentered},
{Gravity.CENTER_HORIZONTAL, horizontallyCentered, 0},
{Gravity.CENTER_VERTICAL, 0, verticallyCentered},
{Gravity.RIGHT, end, 0},
{GravityCompat.END, end, 0},
{Gravity.BOTTOM, 0, bottom},
{Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM, horizontallyCentered, bottom},
{Gravity.RIGHT | Gravity.CENTER_VERTICAL, end, verticallyCentered},
};
for (final int[] testCase : testCases) {
mActivityTestRule.runOnUiThread(new Runnable() {
@Override
public void run() {
final CoordinatorLayout.LayoutParams lp =
(CoordinatorLayout.LayoutParams) view.getLayoutParams();
lp.gravity = testCase[0];
view.setLayoutParams(lp);
}
});
instrumentation.waitForIdleSync();
mActivityTestRule.runOnUiThread(new Runnable() {
@Override
public void run() {
assertThat("Gravity: " + testCase[0], view.getLeft(), is(testCase[1]));
assertThat("Gravity: " + testCase[0], view.getTop(), is(testCase[2]));
}
});
}
}
@Test
public void testInsetDependency() {
final CoordinatorLayout col = mActivityTestRule.getActivity().mCoordinatorLayout;
final CoordinatorLayout.LayoutParams lpInsetLeft = col.generateDefaultLayoutParams();
lpInsetLeft.insetEdge = Gravity.LEFT;
final CoordinatorLayout.LayoutParams lpInsetRight = col.generateDefaultLayoutParams();
lpInsetRight.insetEdge = Gravity.RIGHT;
final CoordinatorLayout.LayoutParams lpInsetTop = col.generateDefaultLayoutParams();
lpInsetTop.insetEdge = Gravity.TOP;
final CoordinatorLayout.LayoutParams lpInsetBottom = col.generateDefaultLayoutParams();
lpInsetBottom.insetEdge = Gravity.BOTTOM;
final CoordinatorLayout.LayoutParams lpDodgeLeft = col.generateDefaultLayoutParams();
lpDodgeLeft.dodgeInsetEdges = Gravity.LEFT;
final CoordinatorLayout.LayoutParams lpDodgeLeftAndTop = col.generateDefaultLayoutParams();
lpDodgeLeftAndTop.dodgeInsetEdges = Gravity.LEFT | Gravity.TOP;
final CoordinatorLayout.LayoutParams lpDodgeAll = col.generateDefaultLayoutParams();
lpDodgeAll.dodgeInsetEdges = Gravity.FILL;
final View a = new View(col.getContext());
final View b = new View(col.getContext());
assertThat(dependsOn(lpDodgeLeft, lpInsetLeft, col, a, b), is(true));
assertThat(dependsOn(lpDodgeLeft, lpInsetRight, col, a, b), is(false));
assertThat(dependsOn(lpDodgeLeft, lpInsetTop, col, a, b), is(false));
assertThat(dependsOn(lpDodgeLeft, lpInsetBottom, col, a, b), is(false));
assertThat(dependsOn(lpDodgeLeftAndTop, lpInsetLeft, col, a, b), is(true));
assertThat(dependsOn(lpDodgeLeftAndTop, lpInsetRight, col, a, b), is(false));
assertThat(dependsOn(lpDodgeLeftAndTop, lpInsetTop, col, a, b), is(true));
assertThat(dependsOn(lpDodgeLeftAndTop, lpInsetBottom, col, a, b), is(false));
assertThat(dependsOn(lpDodgeAll, lpInsetLeft, col, a, b), is(true));
assertThat(dependsOn(lpDodgeAll, lpInsetRight, col, a, b), is(true));
assertThat(dependsOn(lpDodgeAll, lpInsetTop, col, a, b), is(true));
assertThat(dependsOn(lpDodgeAll, lpInsetBottom, col, a, b), is(true));
assertThat(dependsOn(lpInsetLeft, lpDodgeLeft, col, a, b), is(false));
}
private static boolean dependsOn(CoordinatorLayout.LayoutParams lpChild,
CoordinatorLayout.LayoutParams lpDependency, CoordinatorLayout col,
View child, View dependency) {
child.setLayoutParams(lpChild);
dependency.setLayoutParams(lpDependency);
return lpChild.dependsOn(col, child, dependency);
}
@Test
public void testInsetEdge() throws Throwable {
final Instrumentation instrumentation = getInstrumentation();
final CoordinatorLayout col = mActivityTestRule.getActivity().mCoordinatorLayout;
final View insetView = new View(col.getContext());
final View dodgeInsetView = new View(col.getContext());
final AtomicInteger originalTop = new AtomicInteger();
mActivityTestRule.runOnUiThread(new Runnable() {
@Override
public void run() {
CoordinatorLayout.LayoutParams lpInsetView = col.generateDefaultLayoutParams();
lpInsetView.width = CoordinatorLayout.LayoutParams.MATCH_PARENT;
lpInsetView.height = 100;
lpInsetView.gravity = Gravity.TOP | Gravity.LEFT;
lpInsetView.insetEdge = Gravity.TOP;
col.addView(insetView, lpInsetView);
insetView.setBackgroundColor(0xFF0000FF);
CoordinatorLayout.LayoutParams lpDodgeInsetView = col.generateDefaultLayoutParams();
lpDodgeInsetView.width = 100;
lpDodgeInsetView.height = 100;
lpDodgeInsetView.gravity = Gravity.TOP | Gravity.LEFT;
lpDodgeInsetView.dodgeInsetEdges = Gravity.TOP;
col.addView(dodgeInsetView, lpDodgeInsetView);
dodgeInsetView.setBackgroundColor(0xFFFF0000);
}
});
instrumentation.waitForIdleSync();
mActivityTestRule.runOnUiThread(new Runnable() {
@Override
public void run() {
List<View> dependencies = col.getDependencies(dodgeInsetView);
assertThat(dependencies.size(), is(1));
assertThat(dependencies.get(0), is(insetView));
// Move the insetting view
originalTop.set(dodgeInsetView.getTop());
assertThat(originalTop.get(), is(insetView.getBottom()));
ViewCompat.offsetTopAndBottom(insetView, 123);
}
});
instrumentation.waitForIdleSync();
mActivityTestRule.runOnUiThread(new Runnable() {
@Override
public void run() {
// Confirm that the dodging view was moved by the same size
assertThat(dodgeInsetView.getTop() - originalTop.get(), is(123));
}
});
}
@Test
public void testDependentViewChanged() throws Throwable {
final Instrumentation instrumentation = getInstrumentation();
final CoordinatorLayout col = mActivityTestRule.getActivity().mCoordinatorLayout;
// Add two views, A & B, where B depends on A
final View viewA = new View(col.getContext());
final CoordinatorLayout.LayoutParams lpA = col.generateDefaultLayoutParams();
lpA.width = 100;
lpA.height = 100;
final View viewB = new View(col.getContext());
final CoordinatorLayout.LayoutParams lpB = col.generateDefaultLayoutParams();
lpB.width = 100;
lpB.height = 100;
final CoordinatorLayout.Behavior behavior =
spy(new DependentBehavior(viewA));
lpB.setBehavior(behavior);
mActivityTestRule.runOnUiThread(new Runnable() {
@Override
public void run() {
col.addView(viewA, lpA);
col.addView(viewB, lpB);
}
});
instrumentation.waitForIdleSync();
// Reset the Behavior since onDependentViewChanged may have already been called as part of
// any layout/draw passes already
reset(behavior);
// Now offset view A
mActivityTestRule.runOnUiThread(new Runnable() {
@Override
public void run() {
ViewCompat.offsetLeftAndRight(viewA, 20);
ViewCompat.offsetTopAndBottom(viewA, 20);
}
});
instrumentation.waitForIdleSync();
// And assert that view B's Behavior was called appropriately
verify(behavior, times(1)).onDependentViewChanged(col, viewB, viewA);
}
@Test
public void testDependentViewRemoved() throws Throwable {
final Instrumentation instrumentation = getInstrumentation();
final CoordinatorLayout col = mActivityTestRule.getActivity().mCoordinatorLayout;
// Add two views, A & B, where B depends on A
final View viewA = new View(col.getContext());
final View viewB = new View(col.getContext());
final CoordinatorLayout.LayoutParams lpB = col.generateDefaultLayoutParams();
final CoordinatorLayout.Behavior behavior =
spy(new DependentBehavior(viewA));
lpB.setBehavior(behavior);
mActivityTestRule.runOnUiThread(new Runnable() {
@Override
public void run() {
col.addView(viewA);
col.addView(viewB, lpB);
}
});
instrumentation.waitForIdleSync();
// Now remove view A
mActivityTestRule.runOnUiThread(new Runnable() {
@Override
public void run() {
col.removeView(viewA);
}
});
// And assert that View B's Behavior was called appropriately
verify(behavior, times(1)).onDependentViewRemoved(col, viewB, viewA);
}
@Test
public void testGetDependenciesAfterDependentViewRemoved() throws Throwable {
final Instrumentation instrumentation = getInstrumentation();
final CoordinatorLayout col = mActivityTestRule.getActivity().mCoordinatorLayout;
// Add two views, A & B, where B depends on A
final View viewA = new View(col.getContext());
final View viewB = new View(col.getContext());
final CoordinatorLayout.LayoutParams lpB = col.generateDefaultLayoutParams();
final CoordinatorLayout.Behavior behavior =
new CoordinatorLayoutUtils.DependentBehavior(viewA) {
@Override
public void onDependentViewRemoved(
CoordinatorLayout parent, View child, View dependency) {
parent.getDependencies(child);
}
};
lpB.setBehavior(behavior);
// Now add views
mActivityTestRule.runOnUiThread(new Runnable() {
@Override
public void run() {
col.addView(viewA);
col.addView(viewB, lpB);
}
});
// Wait for a layout
instrumentation.waitForIdleSync();
// Now remove view A, which will trigger onDependentViewRemoved() on view B's behavior
mActivityTestRule.runOnUiThread(new Runnable() {
@Override
public void run() {
col.removeView(viewA);
}
});
}
@Test
public void testDodgeInsetBeforeLayout() throws Throwable {
final CoordinatorLayout col = mActivityTestRule.getActivity().mCoordinatorLayout;
// Add a dummy view, which will be used to trigger a hierarchy change.
final View dummy = new View(col.getContext());
mActivityTestRule.runOnUiThread(new Runnable() {
@Override
public void run() {
col.addView(dummy);
}
});
// Wait for a layout.
mInstrumentation.waitForIdleSync();
final View dodge = new View(col.getContext());
final CoordinatorLayout.LayoutParams lpDodge = col.generateDefaultLayoutParams();
lpDodge.dodgeInsetEdges = Gravity.BOTTOM;
lpDodge.setBehavior(new CoordinatorLayout.Behavior() {
@Override
public boolean getInsetDodgeRect(CoordinatorLayout parent, View child, Rect rect) {
// Any non-empty rect is fine here.
rect.set(0, 0, 10, 10);
return true;
}
});
mActivityTestRule.runOnUiThread(new Runnable() {
@Override
public void run() {
col.addView(dodge, lpDodge);
// Ensure the new view is in the list of children.
int heightSpec = MeasureSpec.makeMeasureSpec(col.getHeight(), MeasureSpec.EXACTLY);
int widthSpec = MeasureSpec.makeMeasureSpec(col.getWidth(), MeasureSpec.EXACTLY);
col.measure(widthSpec, heightSpec);
// Force a hierarchy change.
col.removeView(dummy);
}
});
// Wait for a layout.
mInstrumentation.waitForIdleSync();
}
@Test
public void testGoneViewsNotMeasuredLaidOut() throws Throwable {
final CoordinatorLayoutActivity activity = mActivityTestRule.getActivity();
final CoordinatorLayout col = activity.mCoordinatorLayout;
// Now create a GONE view and add it to the CoordinatorLayout
final View imageView = new View(activity);
imageView.setVisibility(View.GONE);
mActivityTestRule.runOnUiThread(new Runnable() {
@Override
public void run() {
col.addView(imageView, 200, 200);
}
});
// Wait for a layout and measure pass
mInstrumentation.waitForIdleSync();
// And assert that it has not been laid out
assertFalse(imageView.getMeasuredWidth() > 0);
assertFalse(imageView.getMeasuredHeight() > 0);
assertFalse(ViewCompat.isLaidOut(imageView));
// Now set the view to INVISIBLE
mActivityTestRule.runOnUiThread(new Runnable() {
@Override
public void run() {
imageView.setVisibility(View.INVISIBLE);
}
});
// Wait for a layout and measure pass
mInstrumentation.waitForIdleSync();
// And assert that it has been laid out
assertTrue(imageView.getMeasuredWidth() > 0);
assertTrue(imageView.getMeasuredHeight() > 0);
assertTrue(ViewCompat.isLaidOut(imageView));
}
@Test
public void testNestedScrollingDispatchesToBehavior() throws Throwable {
final CoordinatorLayoutActivity activity = mActivityTestRule.getActivity();
final CoordinatorLayout col = activity.mCoordinatorLayout;
// Now create a view and add it to the CoordinatorLayout with the spy behavior,
// along with a NestedScrollView
final ImageView imageView = new ImageView(activity);
final CoordinatorLayout.Behavior behavior = spy(new NestedScrollingBehavior());
mActivityTestRule.runOnUiThread(new Runnable() {
@Override
public void run() {
LayoutInflater.from(activity).inflate(R.layout.include_nestedscrollview, col, true);
CoordinatorLayout.LayoutParams clp = new CoordinatorLayout.LayoutParams(200, 200);
clp.setBehavior(behavior);
col.addView(imageView, clp);
}
});
// Now vertically swipe up on the NSV, causing nested scrolling to occur
onView(withId(R.id.nested_scrollview)).perform(swipeUp());
// Verify that the Behavior's onStartNestedScroll was called once
verify(behavior, times(1)).onStartNestedScroll(
eq(col), // parent
eq(imageView), // child
any(View.class), // target
any(View.class), // direct child target
any(int.class)); // axes
// Verify that the Behavior's onNestedScrollAccepted was called once
verify(behavior, times(1)).onNestedScrollAccepted(
eq(col), // parent
eq(imageView), // child
any(View.class), // target
any(View.class), // direct child target
any(int.class)); // axes
// Verify that the Behavior's onNestedPreScroll was called at least once
verify(behavior, atLeastOnce()).onNestedPreScroll(
eq(col), // parent
eq(imageView), // child
any(View.class), // target
any(int.class), // dx
any(int.class), // dy
any(int[].class)); // consumed
// Verify that the Behavior's onNestedScroll was called at least once
verify(behavior, atLeastOnce()).onNestedScroll(
eq(col), // parent
eq(imageView), // child
any(View.class), // target
any(int.class), // dx consumed
any(int.class), // dy consumed
any(int.class), // dx unconsumed
any(int.class)); // dy unconsumed
// Verify that the Behavior's onStopNestedScroll was called once
verify(behavior, times(1)).onStopNestedScroll(
eq(col), // parent
eq(imageView), // child
any(View.class)); // target
}
@Test
public void testNestedScrollingDispatchingToBehaviorWithGoneView() throws Throwable {
final CoordinatorLayoutActivity activity = mActivityTestRule.getActivity();
final CoordinatorLayout col = activity.mCoordinatorLayout;
// Now create a GONE view and add it to the CoordinatorLayout with the spy behavior,
// along with a NestedScrollView
final ImageView imageView = new ImageView(activity);
imageView.setVisibility(View.GONE);
final CoordinatorLayout.Behavior behavior = spy(new NestedScrollingBehavior());
mActivityTestRule.runOnUiThread(new Runnable() {
@Override
public void run() {
LayoutInflater.from(activity).inflate(R.layout.include_nestedscrollview, col, true);
CoordinatorLayout.LayoutParams clp = new CoordinatorLayout.LayoutParams(200, 200);
clp.setBehavior(behavior);
col.addView(imageView, clp);
}
});
// Now vertically swipe up on the NSV, causing nested scrolling to occur
onView(withId(R.id.nested_scrollview)).perform(swipeUp());
// Verify that the Behavior's onStartNestedScroll was not called
verify(behavior, never()).onStartNestedScroll(
eq(col), // parent
eq(imageView), // child
any(View.class), // target
any(View.class), // direct child target
any(int.class)); // axes
// Verify that the Behavior's onNestedScrollAccepted was not called
verify(behavior, never()).onNestedScrollAccepted(
eq(col), // parent
eq(imageView), // child
any(View.class), // target
any(View.class), // direct child target
any(int.class)); // axes
// Verify that the Behavior's onNestedPreScroll was not called
verify(behavior, never()).onNestedPreScroll(
eq(col), // parent
eq(imageView), // child
any(View.class), // target
any(int.class), // dx
any(int.class), // dy
any(int[].class)); // consumed
// Verify that the Behavior's onNestedScroll was not called
verify(behavior, never()).onNestedScroll(
eq(col), // parent
eq(imageView), // child
any(View.class), // target
any(int.class), // dx consumed
any(int.class), // dy consumed
any(int.class), // dx unconsumed
any(int.class)); // dy unconsumed
// Verify that the Behavior's onStopNestedScroll was not called
verify(behavior, never()).onStopNestedScroll(
eq(col), // parent
eq(imageView), // child
any(View.class)); // target
}
@Test
public void testNestedScrollingTriggeringDependentViewChanged() throws Throwable {
final CoordinatorLayoutActivity activity = mActivityTestRule.getActivity();
final CoordinatorLayout col = activity.mCoordinatorLayout;
// First a NestedScrollView to trigger nested scrolling
final View scrollView = LayoutInflater.from(activity).inflate(
R.layout.include_nestedscrollview, col, false);
// Now create a View and Behavior which depend on the scrollview
final ImageView dependentView = new ImageView(activity);
final CoordinatorLayout.Behavior dependentBehavior = spy(new DependentBehavior(scrollView));
// Finally a view which accepts nested scrolling in the CoordinatorLayout
final ImageView nestedScrollAwareView = new ImageView(activity);
mActivityTestRule.runOnUiThread(new Runnable() {
@Override
public void run() {
// First add the ScrollView
col.addView(scrollView);
// Now add the view which depends on the scrollview
CoordinatorLayout.LayoutParams clp = new CoordinatorLayout.LayoutParams(200, 200);
clp.setBehavior(dependentBehavior);
col.addView(dependentView, clp);
// Now add the nested scrolling aware view
clp = new CoordinatorLayout.LayoutParams(200, 200);
clp.setBehavior(new NestedScrollingBehavior());
col.addView(nestedScrollAwareView, clp);
}
});
// Wait for any layouts, and reset the Behavior so that the call counts are 0
getInstrumentation().waitForIdleSync();
reset(dependentBehavior);
// Now vertically swipe up on the NSV, causing nested scrolling to occur
onView(withId(R.id.nested_scrollview)).perform(swipeUp());
// Verify that the Behavior's onDependentViewChanged is not called due to the
// nested scroll
verify(dependentBehavior, never()).onDependentViewChanged(
eq(col), // parent
eq(dependentView), // child
eq(scrollView)); // axes
}
@Test
public void testDodgeInsetViewWithEmptyBounds() throws Throwable {
final CoordinatorLayout col = mActivityTestRule.getActivity().mCoordinatorLayout;
// Add a view with zero height/width which is set to dodge its bounds
final View view = new View(col.getContext());
final CoordinatorLayout.Behavior spyBehavior = spy(new DodgeBoundsBehavior());
mActivityTestRule.runOnUiThread(new Runnable() {
@Override
public void run() {
final CoordinatorLayout.LayoutParams lp = col.generateDefaultLayoutParams();
lp.dodgeInsetEdges = Gravity.BOTTOM;
lp.gravity = Gravity.BOTTOM;
lp.height = 0;
lp.width = 0;
lp.setBehavior(spyBehavior);
col.addView(view, lp);
}
});
// Wait for a layout
mInstrumentation.waitForIdleSync();
// Now add an non-empty bounds inset view to the bottom of the CoordinatorLayout
mActivityTestRule.runOnUiThread(new Runnable() {
@Override
public void run() {
final View dodge = new View(col.getContext());
final CoordinatorLayout.LayoutParams lp = col.generateDefaultLayoutParams();
lp.insetEdge = Gravity.BOTTOM;
lp.gravity = Gravity.BOTTOM;
lp.height = 60;
lp.width = CoordinatorLayout.LayoutParams.MATCH_PARENT;
col.addView(dodge, lp);
}
});
// Verify that the Behavior of the view with empty bounds does not have its
// getInsetDodgeRect() called
verify(spyBehavior, never())
.getInsetDodgeRect(same(col), same(view), any(Rect.class));
}
public static class NestedScrollingBehavior extends CoordinatorLayout.Behavior<View> {
@Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child,
View directTargetChild, View target, int nestedScrollAxes) {
// Return true so that we always accept nested scroll events
return true;
}
}
public static class DodgeBoundsBehavior extends CoordinatorLayout.Behavior<View> {
@Override
public boolean getInsetDodgeRect(CoordinatorLayout parent, View child, Rect rect) {
rect.set(child.getLeft(), child.getTop(), child.getRight(), child.getBottom());
return true;
}
}
@UiThreadTest
@Test
public void testAnchorDependencyGraph() throws Throwable {
final CoordinatorLayout col = mActivityTestRule.getActivity().mCoordinatorLayout;
// Override hashcode because of implementation of SimpleArrayMap used in
// DirectedAcyclicGraph used for sorting dependencies. Hashcode of anchored view has to be
// greater than of the one it is anchored to in order to reproduce the error.
final View anchor = createViewWithHashCode(col.getContext(), 2);
anchor.setId(R.id.anchor);
final View ship = createViewWithHashCode(col.getContext(), 3);
final CoordinatorLayout.LayoutParams lp = col.generateDefaultLayoutParams();
lp.setAnchorId(R.id.anchor);
col.addView(anchor);
col.addView(ship, lp);
// Get dependencies immediately to avoid possible call to onMeasure(), since error
// only happens on first computing of sorted dependencies.
List<View> dependencySortedChildren = col.getDependencySortedChildren();
assertThat(dependencySortedChildren, is(Arrays.asList(anchor, ship)));
}
@NonNull
private View createViewWithHashCode(final Context context, final int hashCode) {
return new View(context) {
@Override
public int hashCode() {
return hashCode;
}
};
}
}