Revert "Revert "Fixed keyboard autoclosing and wrong position for EditTexts" ag/2385121"
This reverts commit 0dbe90863b0569b3770857e3718d360cc8904757.
Fixed the broken tests that were caused by IME kept open when starting
the next test, and put a more reliable wait for waiting on IME popping
up or closing.
Bug: 36082993
Bug: 62373324
Bug: 34887461
Test: ./gradlew support-recyclerview-v7:connectedCheck
-Pandroid.testInstrumentationRunnerArguments.class=android.support.v7.widget.LinearLayoutManagerTest
./gradlew support-recyclerview-v7:connectedCheck
-Pandroid.testInstrumentationRunnerArguments.class=android.support.v7.widget.GridLayoutManagerTest
Change-Id: I53bb76a5fa7e2748b7e0749ce22d642bcee600f0
diff --git a/v7/recyclerview/src/android/support/v7/widget/LinearLayoutManager.java b/v7/recyclerview/src/android/support/v7/widget/LinearLayoutManager.java
index a72975d..a229846 100644
--- a/v7/recyclerview/src/android/support/v7/widget/LinearLayoutManager.java
+++ b/v7/recyclerview/src/android/support/v7/widget/LinearLayoutManager.java
@@ -491,6 +491,7 @@
// resolve layout direction
resolveShouldLayoutReverse();
+ final View focused = getFocusedChild();
if (!mAnchorInfo.mValid || mPendingScrollPosition != NO_POSITION
|| mPendingSavedState != null) {
mAnchorInfo.reset();
@@ -498,6 +499,22 @@
// calculate anchor position and coordinate
updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
mAnchorInfo.mValid = true;
+ } else if (focused != null && (mOrientationHelper.getDecoratedStart(focused)
+ >= mOrientationHelper.getEndAfterPadding()
+ || mOrientationHelper.getDecoratedEnd(focused)
+ <= mOrientationHelper.getStartAfterPadding())) {
+ // This case relates to when the anchor child is the focused view and due to layout
+ // shrinking the focused view fell outside the viewport, e.g. when soft keyboard shows
+ // up after tapping an EditText which shrinks RV causing the focused view (The tapped
+ // EditText which is the anchor child) to get kicked out of the screen. Will update the
+ // anchor coordinate in order to make sure that the focused view is laid out. Otherwise,
+ // the available space in layoutState will be calculated as negative preventing the
+ // focused view from being laid out in fill.
+ // Note that we won't update the anchor position between layout passes (refer to
+ // TestResizingRelayoutWithAutoMeasure), which happens if we were to call
+ // updateAnchorInfoForLayout for an anchor that's not the focused view (e.g. a reference
+ // child which can change between layout passes).
+ mAnchorInfo.assignFromViewAndKeepVisibleRect(focused);
}
if (DEBUG) {
Log.d(TAG, "Anchor info:" + mAnchorInfo);
diff --git a/v7/recyclerview/tests/src/android/support/v7/util/ImeCleanUpTestRule.java b/v7/recyclerview/tests/src/android/support/v7/util/ImeCleanUpTestRule.java
new file mode 100644
index 0000000..f4caad3
--- /dev/null
+++ b/v7/recyclerview/tests/src/android/support/v7/util/ImeCleanUpTestRule.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2017 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 android.support.v7.util;
+
+import android.graphics.Rect;
+import android.support.testutils.PollingCheck;
+import android.view.View;
+
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+/**
+ * A JUnit rule that ensures that IME is closed after a test is finished (or exception thrown).
+ * A test that triggers IME open/close should call setContainerView with the activity's container
+ * view in order to trigger this cleanup at the end of the test. Otherwise, no cleanup happens.
+ */
+public class ImeCleanUpTestRule implements TestRule {
+
+ private View mContainerView;
+
+ @Override
+ public Statement apply(final Statement base, Description description) {
+ return new Statement() {
+ @Override
+ public void evaluate() throws Throwable {
+ try {
+ base.evaluate();
+ } finally {
+ closeImeIfOpen();
+ }
+ }
+ };
+ }
+
+ /**
+ * Sets the container view used to calculate the total screen height and the height available
+ * to the test activity.
+ */
+ public void setContainerView(View containerView) {
+ mContainerView = containerView;
+ }
+
+ private void closeImeIfOpen() {
+ if (mContainerView == null) {
+ return;
+ }
+ // Ensuring that IME is closed after starting each test.
+ final Rect r = new Rect();
+ mContainerView.getWindowVisibleDisplayFrame(r);
+ // This is the entire height of the screen available to both the view and IME
+ final int screenHeight = mContainerView.getRootView().getHeight();
+
+ // r.bottom is the position above IME if it's open or device button.
+ // if IME is shown, r.bottom is smaller than screenHeight.
+ int imeHeight = screenHeight - r.bottom;
+
+ // Picking a threshold to detect when IME is open
+ if (imeHeight > screenHeight * 0.15) {
+ // Soft keyboard is shown, will wait for it to close after running the test. Note that
+ // we don't press back button here as the IME should close by itself when a test
+ // finishes. If the wait isn't done here, the IME can mess up with the layout of the
+ // next test.
+ PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ mContainerView.getWindowVisibleDisplayFrame(r);
+ int imeHeight = screenHeight - r.bottom;
+ return imeHeight < screenHeight * 0.15;
+ }
+ });
+ }
+ }
+}
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/BaseGridLayoutManagerTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/BaseGridLayoutManagerTest.java
index cc6bbe8..13dd1e4 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/BaseGridLayoutManagerTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/BaseGridLayoutManagerTest.java
@@ -325,6 +325,36 @@
}
}
+ class GridEditTextAdapter extends EditTextAdapter {
+
+ Set<Integer> mFullSpanItems = new HashSet<Integer>();
+ int mSpanPerItem = 1;
+
+ GridEditTextAdapter(int count) {
+ this(count, 1);
+ }
+
+ GridEditTextAdapter(int count, int spanPerItem) {
+ super(count);
+ mSpanPerItem = spanPerItem;
+ }
+
+ void setFullSpan(int... items) {
+ for (int i : items) {
+ mFullSpanItems.add(i);
+ }
+ }
+
+ void assignSpanSizeLookup(final GridLayoutManager glm) {
+ glm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
+ @Override
+ public int getSpanSize(int position) {
+ return mFullSpanItems.contains(position) ? glm.getSpanCount() : mSpanPerItem;
+ }
+ });
+ }
+ }
+
class GridTestAdapter extends TestAdapter {
Set<Integer> mFullSpanItems = new HashSet<Integer>();
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewInstrumentationTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewInstrumentationTest.java
index 8ca5f4e..90e0536 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewInstrumentationTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewInstrumentationTest.java
@@ -38,10 +38,12 @@
import android.support.v4.view.ViewCompat;
import android.support.v7.recyclerview.test.R;
import android.support.v7.recyclerview.test.SameActivityTestRule;
+import android.text.Editable;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.TextView;
@@ -793,6 +795,37 @@
}
}
+ public class EditTextAdapter extends RecyclerView.Adapter<TestViewHolder> {
+
+ final ArrayList<Editable> mEditables;
+ public EditTextAdapter(int count) {
+ mEditables = new ArrayList<>();
+ for (int i = 0; i < count; ++i) {
+ mEditables.add(Editable.Factory.getInstance().newEditable("Sample Text " + i));
+ }
+ }
+
+ @Override
+ public TestViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ final EditText editText = new EditText(parent.getContext());
+ editText.setLayoutParams(new ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
+ final TestViewHolder viewHolder = new TestViewHolder(editText);
+ return viewHolder;
+ }
+
+ @Override
+ public void onBindViewHolder(TestViewHolder holder, int position) {
+ ((EditText) holder.itemView).setText(Editable.Factory.getInstance().newEditable(
+ mEditables.get(position)));
+ }
+
+ @Override
+ public int getItemCount() {
+ return mEditables.size();
+ }
+ }
+
public class TestAdapter extends RecyclerView.Adapter<TestViewHolder>
implements AttachDetachCountingAdapter {
@@ -1197,6 +1230,27 @@
}
}
+
+ public static View findFirstFullyVisibleChild(RecyclerView parent) {
+ for (int i = 0; i < parent.getChildCount(); i++) {
+ View child = parent.getChildAt(i);
+ if (isViewFullyInBound(parent, child)) {
+ return child;
+ }
+ }
+ return null;
+ }
+
+ public static View findLastFullyVisibleChild(RecyclerView parent) {
+ for (int i = parent.getChildCount() - 1; i >= 0; i--) {
+ View child = parent.getChildAt(i);
+ if (isViewFullyInBound(parent, child)) {
+ return child;
+ }
+ }
+ return null;
+ }
+
/**
* Returns whether a child of RecyclerView is partially in bound. A child is
* partially in-bounds if it's either fully or partially visible on the screen.
@@ -1232,7 +1286,7 @@
* @param child The child view to be checked whether is fully within RV's bounds.
* @return True if the child view is fully visible; false otherwise.
*/
- public boolean isViewFullyInBound(RecyclerView parent, View child) {
+ public static boolean isViewFullyInBound(RecyclerView parent, View child) {
if (child == null) {
return false;
}
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/GridLayoutManagerTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/GridLayoutManagerTest.java
index 5ebebfe..23eaf52 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/GridLayoutManagerTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/GridLayoutManagerTest.java
@@ -18,6 +18,9 @@
import static android.support.v7.widget.LinearLayoutManager.HORIZONTAL;
import static android.support.v7.widget.LinearLayoutManager.VERTICAL;
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
+import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertEquals;
@@ -34,15 +37,20 @@
import android.support.test.filters.LargeTest;
import android.support.test.filters.SdkSuppress;
import android.support.test.runner.AndroidJUnit4;
+import android.support.testutils.PollingCheck;
import android.support.v4.view.AccessibilityDelegateCompat;
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
+import android.support.v7.util.ImeCleanUpTestRule;
+import android.support.v7.util.TouchUtils;
import android.test.UiThreadTest;
import android.util.SparseIntArray;
import android.util.StateSet;
+import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import org.hamcrest.CoreMatchers;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -56,6 +64,9 @@
@RunWith(AndroidJUnit4.class)
public class GridLayoutManagerTest extends BaseGridLayoutManagerTest {
+ @Rule
+ public final ImeCleanUpTestRule imeCleanUp = new ImeCleanUpTestRule();
+
@Test
public void focusSearchFailureUp() throws Throwable {
focusSearchFailure(false);
@@ -168,6 +179,101 @@
}
}
+ @Test
+ public void editTextVisibility() throws Throwable {
+ final int spanCount = 3;
+ final int itemCount = 100;
+
+ imeCleanUp.setContainerView(getActivity().getContainer());
+ RecyclerView recyclerView = new WrappedRecyclerView(getActivity());
+ GridEditTextAdapter editTextAdapter = new GridEditTextAdapter(itemCount) {
+ @Override
+ public TestViewHolder onCreateViewHolder(ViewGroup parent,
+ int viewType) {
+ TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType);
+ // Good to have colors for debugging
+ StateListDrawable stl = new StateListDrawable();
+ stl.addState(new int[]{android.R.attr.state_focused},
+ new ColorDrawable(Color.RED));
+ stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE));
+ //noinspection deprecation using this for kitkat tests
+ testViewHolder.itemView.setBackgroundDrawable(stl);
+ return testViewHolder;
+ }
+ };
+ mActivityRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mActivityRule.getActivity().getWindow().setSoftInputMode(SOFT_INPUT_ADJUST_RESIZE);
+ }
+ });
+
+ recyclerView.setLayoutParams(
+ new ViewGroup.LayoutParams(MATCH_PARENT, WRAP_CONTENT));
+
+ Config config = new Config(spanCount, itemCount);
+ mGlm = new WrappedGridLayoutManager(getActivity(), config.mSpanCount, config.mOrientation,
+ config.mReverseLayout);
+ editTextAdapter.assignSpanSizeLookup(mGlm);
+ recyclerView.setAdapter(editTextAdapter);
+ recyclerView.setLayoutManager(mGlm);
+ waitForFirstLayout(recyclerView);
+
+ // First focus on the last fully visible EditText located at span index #1.
+ View toFocus = findLastFullyVisibleChild(mRecyclerView);
+ int focusIndex = mRecyclerView.getChildAdapterPosition(toFocus);
+ focusIndex = (focusIndex / spanCount) * spanCount + 1;
+ toFocus = mRecyclerView.findViewHolderForAdapterPosition(focusIndex).itemView;
+ assertTrue(focusIndex >= 1 && focusIndex < itemCount);
+
+ final int heightBeforeImeOpen = mRecyclerView.getHeight();
+ TouchUtils.tapView(getInstrumentation(), mRecyclerView, toFocus);
+ getInstrumentation().waitForIdleSync();
+ // Wait for IME to pop up.
+ PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return mRecyclerView.getHeight() < heightBeforeImeOpen;
+ }
+ });
+
+ assertThat("Child at position " + focusIndex + " should be focused",
+ toFocus.hasFocus(), is(true));
+ assertTrue("Child view at adapter pos " + focusIndex + " should be fully visible.",
+ isViewPartiallyInBound(mRecyclerView, toFocus));
+
+ // Close IME
+ final int heightBeforeImeClose = mRecyclerView.getHeight();
+ getInstrumentation().sendKeyDownUpSync(KeyEvent.KEYCODE_BACK);
+ getInstrumentation().waitForIdleSync();
+ // Wait for IME to close
+ PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return mRecyclerView.getHeight() > heightBeforeImeClose;
+ }
+ });
+ assertTrue("Child view at adapter pos " + focusIndex + " should be fully visible.",
+ isViewPartiallyInBound(mRecyclerView, toFocus));
+
+ // Now focus on the first fully visible EditText located at the last span index.
+ toFocus = findFirstFullyVisibleChild(mRecyclerView);
+ focusIndex = mRecyclerView.getChildAdapterPosition(toFocus);
+ focusIndex = (focusIndex / spanCount) * spanCount + (spanCount - 1);
+ toFocus = mRecyclerView.findViewHolderForAdapterPosition(focusIndex).itemView;
+ final int heightBeforeImeOpen2 = mRecyclerView.getHeight();
+ TouchUtils.tapView(getInstrumentation(), mRecyclerView, toFocus);
+ getInstrumentation().waitForIdleSync();
+ // Wait for IME to pop up
+ PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return mRecyclerView.getHeight() < heightBeforeImeOpen2;
+ }
+ });
+ assertTrue("Child view at adapter pos " + focusIndex + " should be fully visible.",
+ isViewPartiallyInBound(mRecyclerView, toFocus));
+ }
@Test
public void topUnfocusableViewsVisibility() throws Throwable {
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/LinearLayoutManagerTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/LinearLayoutManagerTest.java
index a2db640..91d0dbf 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/LinearLayoutManagerTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/LinearLayoutManagerTest.java
@@ -18,6 +18,9 @@
import static android.support.v7.widget.LinearLayoutManager.HORIZONTAL;
import static android.support.v7.widget.LinearLayoutManager.VERTICAL;
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
+import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertEquals;
@@ -34,13 +37,19 @@
import android.os.Build;
import android.support.test.filters.LargeTest;
import android.support.test.filters.SdkSuppress;
+import android.support.testutils.PollingCheck;
import android.support.v4.view.AccessibilityDelegateCompat;
+import android.support.v7.util.ImeCleanUpTestRule;
+import android.support.v7.util.TouchUtils;
import android.util.Log;
import android.util.StateSet;
+import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
+import android.widget.LinearLayout;
+import org.junit.Rule;
import org.junit.Test;
import java.util.ArrayList;
@@ -60,6 +69,111 @@
@LargeTest
public class LinearLayoutManagerTest extends BaseLinearLayoutManagerTest {
+ @Rule
+ public final ImeCleanUpTestRule imeCleanUp = new ImeCleanUpTestRule();
+
+ @Test
+ public void editTextVisibility() throws Throwable {
+
+ // Simulating a scenario where an EditText is tapped (which will receive focus).
+ // The soft keyboard that's opened overlaps the focused EditText which will shrink RV's
+ // padded bounded area. LLM should still lay out the focused EditText so that it becomes
+ // visible above the soft keyboard.
+ // The condition for this test is setting RV's height to a non-exact height, so that measure
+ // is called twice (once with the larger height and another time with smaller height when
+ // the keyboard shows up). To ensure this resizing of RV, SOFT_INPUT_ADJUST_RESIZE is set.
+ imeCleanUp.setContainerView(getActivity().getContainer());
+ final LinearLayout container = new LinearLayout(getActivity());
+ container.setOrientation(LinearLayout.VERTICAL);
+ container.setLayoutParams(
+ new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup
+ .LayoutParams.MATCH_PARENT));
+
+ final EditTextAdapter editTextAdapter = new EditTextAdapter(50);
+
+ mRecyclerView = inflateWrappedRV();
+ ViewGroup.LayoutParams lp = mRecyclerView.getLayoutParams();
+ lp.height = WRAP_CONTENT;
+ lp.width = MATCH_PARENT;
+
+ mRecyclerView.setHasFixedSize(true);
+ mRecyclerView.setAdapter(editTextAdapter);
+ mLayoutManager = new WrappedLinearLayoutManager(getActivity(), VERTICAL, false);
+ mRecyclerView.setLayoutManager(mLayoutManager);
+
+ container.addView(mRecyclerView);
+
+ mLayoutManager.expectLayouts(1);
+ mActivityRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ getActivity().getContainer().addView(container);
+ }
+ });
+ mActivityRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mActivityRule.getActivity().getWindow().setSoftInputMode(SOFT_INPUT_ADJUST_RESIZE);
+ }
+ });
+
+ // First focus on the last fully visible EditText.
+ View toFocus = findLastFullyVisibleChild(mRecyclerView);
+ int focusIndex = mRecyclerView.getChildAdapterPosition(toFocus);
+
+ final int heightBeforeImeOpen = mRecyclerView.getHeight();
+ TouchUtils.tapView(getInstrumentation(), mRecyclerView, toFocus);
+ getInstrumentation().waitForIdleSync();
+ // Wait for IME to pop up.
+ PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return mRecyclerView.getHeight() < heightBeforeImeOpen;
+ }
+ });
+
+ assertThat("Child at position " + focusIndex + " should be focused",
+ toFocus.hasFocus(), is(true));
+ // Testing for partial visibility instead of full visibility since TextView calls
+ // requestRectangleOnScreen (inside bringPointIntoView) for the focused view with a rect
+ // containing the content area. This rect is guaranteed to be fully visible whereas a
+ // portion of TextView could be out of bounds.
+ assertTrue("Child view at adapter pos " + focusIndex + " should be fully visible.",
+ isViewPartiallyInBound(mRecyclerView, toFocus));
+
+ // Close IME
+ final int heightBeforeImeClose = mRecyclerView.getHeight();
+ getInstrumentation().sendKeyDownUpSync(KeyEvent.KEYCODE_BACK);
+ getInstrumentation().waitForIdleSync();
+ // Wait for IME to close
+ PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return mRecyclerView.getHeight() > heightBeforeImeClose;
+ }
+ });
+ assertThat("Child at position " + focusIndex + " should be focused",
+ toFocus.hasFocus(), is(true));
+ assertTrue("Child view at adapter pos " + focusIndex + " should be fully visible.",
+ isViewPartiallyInBound(mRecyclerView, toFocus));
+
+ // Now focus on the first fully visible EditText.
+ toFocus = findFirstFullyVisibleChild(mRecyclerView);
+ focusIndex = mRecyclerView.getChildAdapterPosition(toFocus);
+ final int heightBeforeImeOpen2 = mRecyclerView.getHeight();
+ TouchUtils.tapView(getInstrumentation(), mRecyclerView, toFocus);
+ getInstrumentation().waitForIdleSync();
+ // Wait for IME to pop up
+ PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return mRecyclerView.getHeight() < heightBeforeImeOpen2;
+ }
+ });
+ assertTrue("Child view at adapter pos " + focusIndex + " should be fully visible.",
+ isViewPartiallyInBound(mRecyclerView, toFocus));
+ }
+
@Test
public void topUnfocusableViewsVisibility() throws Throwable {
// The maximum number of child views that can be visible at any time.