blob: 11e921ab55941d69ebca136060762b654d6c2252 [file] [log] [blame]
/*
* Copyright (C) 2008 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.view.cts;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import android.graphics.Rect;
import android.view.FocusFinder;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.FrameLayout;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.MediumTest;
import androidx.test.rule.ActivityTestRule;
import androidx.test.runner.AndroidJUnit4;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@MediumTest
@RunWith(AndroidJUnit4.class)
public class FocusFinderTest {
private FocusFinder mFocusFinder;
private ViewGroup mLayout;
private Button mTopLeft;
private Button mTopRight;
private Button mBottomLeft;
private Button mBottomRight;
@Rule
public ActivityTestRule<FocusFinderCtsActivity> mActivityRule =
new ActivityTestRule<>(FocusFinderCtsActivity.class);
@Before
public void setup() {
FocusFinderCtsActivity activity = mActivityRule.getActivity();
mFocusFinder = FocusFinder.getInstance();
mLayout = activity.layout;
mTopLeft = activity.topLeftButton;
mTopRight = activity.topRightButton;
mBottomLeft = activity.bottomLeftButton;
mBottomRight = activity.bottomRightButton;
mTopLeft.setNextFocusLeftId(View.NO_ID);
mTopRight.setNextFocusLeftId(View.NO_ID);
mBottomLeft.setNextFocusLeftId(View.NO_ID);
mBottomRight.setNextFocusLeftId(View.NO_ID);
}
@Test
public void testGetInstance() {
assertNotNull(mFocusFinder);
}
@Test
public void testFindNextFocus() throws Throwable {
/*
* Go clockwise around the buttons from the top left searching for focus.
*
* +---+---+
* | 1 | 2 |
* +---+---+
* | 3 | 4 |
* +---+---+
*/
verifyNextFocus(mTopLeft, View.FOCUS_RIGHT, mTopRight);
verifyNextFocus(mTopRight, View.FOCUS_DOWN, mBottomRight);
verifyNextFocus(mBottomRight, View.FOCUS_LEFT, mBottomLeft);
verifyNextFocus(mBottomLeft, View.FOCUS_UP, mTopLeft);
verifyNextFocus(null, View.FOCUS_RIGHT, mTopLeft);
verifyNextFocus(null, View.FOCUS_DOWN, mTopLeft);
verifyNextFocus(null, View.FOCUS_LEFT, mBottomRight);
verifyNextFocus(null, View.FOCUS_UP, mBottomRight);
// Check that left/right traversal works when top/bottom borders are equal.
verifyNextFocus(mTopRight, View.FOCUS_LEFT, mTopLeft);
verifyNextFocus(mBottomLeft, View.FOCUS_RIGHT, mBottomRight);
// Edge-case where root has focus
mActivityRule.runOnUiThread(() -> {
mLayout.setFocusableInTouchMode(true);
verifyNextFocus(mLayout, View.FOCUS_FORWARD, mTopLeft);
});
}
private void verifyNextFocus(View currentFocus, int direction, View expectedNextFocus) {
View actualNextFocus = mFocusFinder.findNextFocus(mLayout, currentFocus, direction);
assertEquals(expectedNextFocus, actualNextFocus);
}
@Test
public void testFindNextFocusFromRect() {
/*
* Create a small rectangle on the border between the top left and top right buttons.
*
* +---+---+
* | [ ] |
* +---+---+
* | | |
* +---+---+
*/
int buttonHalfWidth = mTopLeft.getWidth() / 2;
Rect topRect = new Rect(mTopLeft.getLeft() + buttonHalfWidth,
mTopLeft.getTop(),
mTopLeft.getRight() + buttonHalfWidth,
mTopLeft.getBottom());
verifytNextFocusFromRect(topRect, View.FOCUS_LEFT, mTopLeft);
verifytNextFocusFromRect(topRect, View.FOCUS_RIGHT, mTopRight);
/*
* Create a small rectangle on the border between the top left and bottom left buttons.
*
* +---+---+
* | | |
* +[ ]+---+
* | | |
* +---+---+
*/
int buttonHalfHeight = mTopLeft.getHeight() / 2;
Rect leftRect = new Rect(mTopLeft.getLeft(),
mTopLeft.getTop() + buttonHalfHeight,
mTopLeft.getRight(),
mTopLeft.getBottom() + buttonHalfHeight);
verifytNextFocusFromRect(leftRect, View.FOCUS_UP, mTopLeft);
verifytNextFocusFromRect(leftRect, View.FOCUS_DOWN, mBottomLeft);
}
private void verifytNextFocusFromRect(Rect rect, int direction, View expectedNextFocus) {
View actualNextFocus = mFocusFinder.findNextFocusFromRect(mLayout, rect, direction);
assertEquals(expectedNextFocus, actualNextFocus);
}
@Test
public void testFindNearestTouchable() {
/*
* Table layout with two rows and coordinates are relative to those parent rows.
* Lines outside the box signify touch points used in the tests.
* |
* +---+---+
* | 1 | 2 |--
* +---+---+
* --| 3 | 4 |
* +---+---+
* |
*/
// 1
int x = mTopLeft.getWidth() / 2 - 5;
int y = 0;
int[] deltas = new int[2];
View view = mFocusFinder.findNearestTouchable(mLayout, x, y, View.FOCUS_DOWN, deltas);
assertEquals(mTopLeft, view);
assertEquals(0, deltas[0]);
assertEquals(0, deltas[1]);
// 2
deltas = new int[2];
x = mTopRight.getRight();
y = mTopRight.getBottom() / 2;
view = mFocusFinder.findNearestTouchable(mLayout, x, y, View.FOCUS_LEFT, deltas);
assertEquals(mTopRight, view);
assertEquals(-1, deltas[0]);
assertEquals(0, deltas[1]);
// 3
deltas = new int[2];
x = 0;
y = mTopLeft.getBottom() + mBottomLeft.getHeight() / 2;
view = mFocusFinder.findNearestTouchable(mLayout, x, y, View.FOCUS_RIGHT, deltas);
assertEquals(mBottomLeft, view);
assertEquals(0, deltas[0]);
assertEquals(0, deltas[1]);
// 4
deltas = new int[2];
x = mBottomRight.getRight();
y = mTopRight.getBottom() + mBottomRight.getBottom();
view = mFocusFinder.findNearestTouchable(mLayout, x, y, View.FOCUS_UP, deltas);
assertEquals(mBottomRight, view);
assertEquals(0, deltas[0]);
assertEquals(-1, deltas[1]);
}
@Test
public void testFindNextAndPrevFocusAvoidingChain() {
mBottomRight.setNextFocusForwardId(mBottomLeft.getId());
mBottomLeft.setNextFocusForwardId(mTopRight.getId());
// Follow the chain
verifyNextFocus(mBottomRight, View.FOCUS_FORWARD, mBottomLeft);
verifyNextFocus(mBottomLeft, View.FOCUS_FORWARD, mTopRight);
verifyNextFocus(mTopRight, View.FOCUS_BACKWARD, mBottomLeft);
verifyNextFocus(mBottomLeft, View.FOCUS_BACKWARD, mBottomRight);
// Now go to the one not in the chain
verifyNextFocus(mTopRight, View.FOCUS_FORWARD, mTopLeft);
verifyNextFocus(mBottomRight, View.FOCUS_BACKWARD, mTopLeft);
// Now go back to the top of the chain
verifyNextFocus(mTopLeft, View.FOCUS_FORWARD, mBottomRight);
verifyNextFocus(mTopLeft, View.FOCUS_BACKWARD, mTopRight);
// Now make the chain a circle -- this is the pathological case
mTopRight.setNextFocusForwardId(mBottomRight.getId());
// Fall back to the next one in a chain.
verifyNextFocus(mTopLeft, View.FOCUS_FORWARD, mTopRight);
verifyNextFocus(mTopLeft, View.FOCUS_BACKWARD, mBottomRight);
//Now do branching focus changes
mTopRight.setNextFocusForwardId(View.NO_ID);
mBottomRight.setNextFocusForwardId(mTopRight.getId());
verifyNextFocus(mBottomRight, View.FOCUS_FORWARD, mTopRight);
verifyNextFocus(mBottomLeft, View.FOCUS_FORWARD, mTopRight);
// From the tail, it jumps out of the chain
verifyNextFocus(mTopRight, View.FOCUS_FORWARD, mTopLeft);
// Back from the head of a tree goes out of the tree
// We don't know which is the head of the focus chain since it is branching.
View prevFocus1 = mFocusFinder.findNextFocus(mLayout, mBottomLeft, View.FOCUS_BACKWARD);
View prevFocus2 = mFocusFinder.findNextFocus(mLayout, mBottomRight, View.FOCUS_BACKWARD);
assertTrue(prevFocus1 == mTopLeft || prevFocus2 == mTopLeft);
// From outside, it chooses an arbitrary head of the chain
View nextFocus = mFocusFinder.findNextFocus(mLayout, mTopLeft, View.FOCUS_FORWARD);
assertTrue(nextFocus == mBottomRight || nextFocus == mBottomLeft);
// Going back from the tail of the split chain, it chooses an arbitrary head
nextFocus = mFocusFinder.findNextFocus(mLayout, mTopRight, View.FOCUS_BACKWARD);
assertTrue(nextFocus == mBottomRight || nextFocus == mBottomLeft);
}
@Test(timeout = 500)
public void testChainVisibility() {
mBottomRight.setNextFocusForwardId(mBottomLeft.getId());
mBottomLeft.setNextFocusForwardId(mTopRight.getId());
mBottomLeft.setVisibility(View.INVISIBLE);
View next = mFocusFinder.findNextFocus(mLayout, mBottomRight, View.FOCUS_FORWARD);
assertSame(mTopRight, next);
mBottomLeft.setNextFocusForwardId(View.NO_ID);
next = mFocusFinder.findNextFocus(mLayout, mBottomRight, View.FOCUS_FORWARD);
assertSame(mTopLeft, next);
// This shouldn't go into an infinite loop
mBottomRight.setNextFocusForwardId(mTopRight.getId());
mTopLeft.setNextFocusForwardId(mTopRight.getId());
mTopRight.setNextFocusForwardId(mBottomLeft.getId());
mBottomLeft.setNextFocusForwardId(mTopLeft.getId());
mActivityRule.getActivity().runOnUiThread(() -> {
mTopLeft.setVisibility(View.INVISIBLE);
mTopRight.setVisibility(View.INVISIBLE);
mBottomLeft.setVisibility(View.INVISIBLE);
mBottomRight.setVisibility(View.INVISIBLE);
});
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
mFocusFinder.findNextFocus(mLayout, mBottomRight, View.FOCUS_FORWARD);
}
private void verifyNextCluster(View currentCluster, int direction, View expectedNextCluster) {
View actualNextCluster = mFocusFinder.findNextKeyboardNavigationCluster(
mLayout, currentCluster, direction);
assertEquals(expectedNextCluster, actualNextCluster);
}
private void verifyNextClusterView(View currentCluster, int direction, View expectedNextView) {
View actualNextView = mFocusFinder.findNextKeyboardNavigationCluster(
mLayout, currentCluster, direction);
if (actualNextView == mLayout) {
actualNextView =
mFocusFinder.findNextKeyboardNavigationCluster(mLayout, null, direction);
}
assertEquals(expectedNextView, actualNextView);
}
@Test
public void testNoClusters() {
// No views are marked as clusters, so next cluster is always null.
verifyNextCluster(mTopRight, View.FOCUS_FORWARD, null);
verifyNextCluster(mTopRight, View.FOCUS_BACKWARD, null);
}
@Test
public void testFindNextCluster() {
// Cluster navigation from all possible starting points in all directions.
mTopLeft.setKeyboardNavigationCluster(true);
mTopRight.setKeyboardNavigationCluster(true);
mBottomLeft.setKeyboardNavigationCluster(true);
verifyNextCluster(null, View.FOCUS_FORWARD, mTopLeft);
verifyNextCluster(mTopLeft, View.FOCUS_FORWARD, mTopRight);
verifyNextCluster(mTopRight, View.FOCUS_FORWARD, mBottomLeft);
verifyNextCluster(mBottomLeft, View.FOCUS_FORWARD, mLayout);
verifyNextCluster(mBottomRight, View.FOCUS_FORWARD, mLayout);
verifyNextCluster(null, View.FOCUS_BACKWARD, mBottomLeft);
verifyNextCluster(mTopLeft, View.FOCUS_BACKWARD, mLayout);
verifyNextCluster(mTopRight, View.FOCUS_BACKWARD, mTopLeft);
verifyNextCluster(mBottomLeft, View.FOCUS_BACKWARD, mTopRight);
verifyNextCluster(mBottomRight, View.FOCUS_BACKWARD, mLayout);
}
@Test
public void testFindNextAndPrevClusterAvoidingChain() {
// Basically a duplicate of normal focus test above. The same logic should be used for both.
mTopLeft.setKeyboardNavigationCluster(true);
mTopRight.setKeyboardNavigationCluster(true);
mBottomLeft.setKeyboardNavigationCluster(true);
mBottomRight.setKeyboardNavigationCluster(true);
mBottomRight.setNextClusterForwardId(mBottomLeft.getId());
mBottomLeft.setNextClusterForwardId(mTopRight.getId());
// Follow the chain
verifyNextCluster(mBottomRight, View.FOCUS_FORWARD, mBottomLeft);
verifyNextCluster(mBottomLeft, View.FOCUS_FORWARD, mTopRight);
verifyNextCluster(mTopRight, View.FOCUS_BACKWARD, mBottomLeft);
verifyNextCluster(mBottomLeft, View.FOCUS_BACKWARD, mBottomRight);
// Now go to the one not in the chain
verifyNextClusterView(mTopRight, View.FOCUS_FORWARD, mTopLeft);
verifyNextClusterView(mBottomRight, View.FOCUS_BACKWARD, mTopLeft);
// Now go back to the top of the chain
verifyNextClusterView(mTopLeft, View.FOCUS_FORWARD, mBottomRight);
verifyNextClusterView(mTopLeft, View.FOCUS_BACKWARD, mTopRight);
// Now make the chain a circle -- this is the pathological case
mTopRight.setNextClusterForwardId(mBottomRight.getId());
// Fall back to the next one in a chain.
verifyNextClusterView(mTopLeft, View.FOCUS_FORWARD, mTopRight);
verifyNextClusterView(mTopLeft, View.FOCUS_BACKWARD, mBottomRight);
//Now do branching focus changes
mTopRight.setNextClusterForwardId(View.NO_ID);
mBottomRight.setNextClusterForwardId(mTopRight.getId());
assertEquals(mBottomRight.getNextClusterForwardId(), mTopRight.getId());
verifyNextClusterView(mBottomRight, View.FOCUS_FORWARD, mTopRight);
verifyNextClusterView(mBottomLeft, View.FOCUS_FORWARD, mTopRight);
// From the tail, it jumps out of the chain
verifyNextClusterView(mTopRight, View.FOCUS_FORWARD, mTopLeft);
// Back from the head of a tree goes out of the tree
// We don't know which is the head of the focus chain since it is branching.
View prevFocus1 = mFocusFinder.findNextKeyboardNavigationCluster(mLayout, mBottomLeft,
View.FOCUS_BACKWARD);
View prevFocus2 = mFocusFinder.findNextKeyboardNavigationCluster(mLayout, mBottomRight,
View.FOCUS_BACKWARD);
assertTrue(prevFocus1 == mTopLeft || prevFocus2 == mTopLeft);
// From outside, it chooses an arbitrary head of the chain
View nextFocus = mFocusFinder.findNextKeyboardNavigationCluster(mLayout, mTopLeft,
View.FOCUS_FORWARD);
assertTrue(nextFocus == mBottomRight || nextFocus == mBottomLeft);
// Going back from the tail of the split chain, it chooses an arbitrary head
nextFocus = mFocusFinder.findNextKeyboardNavigationCluster(mLayout, mTopRight,
View.FOCUS_BACKWARD);
assertTrue(nextFocus == mBottomRight || nextFocus == mBottomLeft);
}
@Test
public void testDuplicateId() throws Throwable {
LayoutInflater inflater = mActivityRule.getActivity().getLayoutInflater();
mLayout = (ViewGroup) mActivityRule.getActivity().findViewById(R.id.inflate_layout);
View[] buttons = new View[3];
View[] boxes = new View[3];
mActivityRule.runOnUiThread(() -> {
for (int i = 0; i < 3; ++i) {
View item = inflater.inflate(R.layout.focus_finder_sublayout, mLayout, false);
buttons[i] = item.findViewById(R.id.itembutton);
boxes[i] = item.findViewById(R.id.itembox);
mLayout.addView(item);
}
});
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
verifyNextFocus(buttons[0], View.FOCUS_FORWARD, boxes[0]);
verifyNextFocus(boxes[0], View.FOCUS_FORWARD, buttons[1]);
verifyNextFocus(buttons[1], View.FOCUS_FORWARD, boxes[1]);
verifyNextFocus(boxes[1], View.FOCUS_FORWARD, buttons[2]);
}
@Test
public void testBasicFocusOrder() {
// Sanity check to make sure sorter is behaving
FrameLayout layout = new FrameLayout(mLayout.getContext());
Button button1 = new Button(mLayout.getContext());
Button button2 = new Button(mLayout.getContext());
setViewBox(button1, 0, 0, 10, 10);
setViewBox(button2, 0, 0, 10, 10);
layout.addView(button1);
layout.addView(button2);
View[] views = new View[]{button2, button1};
// empty shouldn't crash or anything
FocusFinder.sort(views, 0, 0, layout, false);
// one view should work
FocusFinder.sort(views, 0, 1, layout, false);
assertEquals(button2, views[0]);
// exactly overlapping views should remain in original order
FocusFinder.sort(views, 0, 2, layout, false);
assertEquals(button2, views[0]);
assertEquals(button1, views[1]);
// make sure it will actually mutate input array.
setViewBox(button2, 20, 0, 30, 10);
FocusFinder.sort(views, 0, 2, layout, false);
assertEquals(button1, views[0]);
assertEquals(button2, views[1]);
// While we don't want to test details, we should at least verify basic correctness
// like "left-to-right" ordering in well-behaved layouts
verifyNextFocus(mTopLeft, View.FOCUS_FORWARD, mTopRight);
verifyNextFocus(mTopRight, View.FOCUS_FORWARD, mBottomLeft);
verifyNextFocus(mBottomLeft, View.FOCUS_FORWARD, mBottomRight);
// Should still work intuitively even if some views are slightly shorter.
mBottomLeft.setBottom(mBottomLeft.getBottom() - 3);
mBottomLeft.offsetTopAndBottom(3);
verifyNextFocus(mTopLeft, View.FOCUS_FORWARD, mTopRight);
verifyNextFocus(mTopRight, View.FOCUS_FORWARD, mBottomLeft);
verifyNextFocus(mBottomLeft, View.FOCUS_FORWARD, mBottomRight);
// RTL layout should work right-to-left
mActivityRule.getActivity().runOnUiThread(
() -> mLayout.setLayoutDirection(View.LAYOUT_DIRECTION_RTL));
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
verifyNextFocus(mTopLeft, View.FOCUS_FORWARD, mTopRight);
verifyNextFocus(mTopRight, View.FOCUS_FORWARD, mBottomLeft);
verifyNextFocus(mBottomLeft, View.FOCUS_FORWARD, mBottomRight);
}
private void setViewBox(View view, int left, int top, int right, int bottom) {
view.setLeft(left);
view.setTop(top);
view.setRight(right);
view.setBottom(bottom);
}
}