| /* |
| * 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.support.test.InstrumentationRegistry; |
| import android.support.test.filters.MediumTest; |
| import android.support.test.rule.ActivityTestRule; |
| import android.support.test.runner.AndroidJUnit4; |
| 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 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); |
| } |
| } |