| /* |
| * 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; |
| |
| import android.graphics.Rect; |
| import android.test.AndroidTestCase; |
| |
| import androidx.test.filters.SmallTest; |
| |
| public class FocusFinderTest extends AndroidTestCase { |
| |
| private FocusFinderHelper mFocusFinder; |
| |
| @Override |
| protected void setUp() throws Exception { |
| super.setUp(); |
| |
| mFocusFinder = new FocusFinderHelper(FocusFinder.getInstance()); |
| } |
| |
| @SmallTest |
| public void testPreconditions() { |
| assertNotNull("focus finder instance", mFocusFinder); |
| } |
| |
| @SmallTest |
| public void testBelowNotCandidateForDirectionUp() { |
| assertIsNotCandidate(View.FOCUS_UP, |
| new Rect(0, 30, 10, 40), // src (left, top, right, bottom) |
| new Rect(0, 50, 10, 60)); // dest (left, top, right, bottom) |
| } |
| |
| @SmallTest |
| public void testAboveShareEdgeEdgeOkForDirectionUp() { |
| final Rect src = new Rect(0, 30, 10, 40); |
| |
| final Rect dest = new Rect(src); |
| dest.offset(0, -src.height()); |
| assertEquals(src.top, dest.bottom); |
| |
| assertDirectionIsCandidate(View.FOCUS_UP, src, dest); |
| } |
| |
| @SmallTest |
| public void testCompletelyContainedNotCandidate() { |
| assertIsNotCandidate( |
| View.FOCUS_DOWN, |
| // L T R B |
| new Rect(0, 0, 50, 50), |
| new Rect(0, 1, 50, 49)); |
| } |
| |
| @SmallTest |
| public void testContinaedWithCommonBottomNotCandidate() { |
| assertIsNotCandidate( |
| View.FOCUS_DOWN, |
| // L T R B |
| new Rect(0, 0, 50, 50), |
| new Rect(0, 1, 50, 50)); |
| } |
| |
| @SmallTest |
| public void testOverlappingIsCandidateWhenBothEdgesAreInDirection() { |
| assertDirectionIsCandidate( |
| View.FOCUS_DOWN, |
| // L T R B |
| new Rect(0, 0, 50, 50), |
| new Rect(0, 1, 50, 51)); |
| } |
| |
| @SmallTest |
| public void testTopEdgeOfDestAtOrAboveTopOfSrcNotCandidateForDown() { |
| assertIsNotCandidate( |
| View.FOCUS_DOWN, |
| // L T R B |
| new Rect(0, 0, 50, 50), |
| new Rect(0, 0, 50, 51)); |
| assertIsNotCandidate( |
| View.FOCUS_DOWN, |
| // L T R B |
| new Rect(0, 0, 50, 50), |
| new Rect(0, -1, 50, 51)); |
| } |
| |
| @SmallTest |
| public void testSameRectBeamsOverlap() { |
| final Rect rect = new Rect(0, 0, 20, 20); |
| |
| assertBeamsOverlap(View.FOCUS_LEFT, rect, rect); |
| assertBeamsOverlap(View.FOCUS_RIGHT, rect, rect); |
| assertBeamsOverlap(View.FOCUS_UP, rect, rect); |
| assertBeamsOverlap(View.FOCUS_DOWN, rect, rect); |
| } |
| |
| @SmallTest |
| public void testOverlapBeamsRightLeftUpToEdge() { |
| final Rect rect1 = new Rect(0, 0, 20, 20); |
| final Rect rect2 = new Rect(rect1); |
| |
| // just below bottom edge |
| rect2.offset(0, rect1.height() - 1); |
| assertBeamsOverlap(View.FOCUS_LEFT, rect1, rect2); |
| assertBeamsOverlap(View.FOCUS_RIGHT, rect1, rect2); |
| |
| // at edge |
| rect2.offset(0, 1); |
| assertBeamsDontOverlap(View.FOCUS_LEFT, rect1, rect2); |
| assertBeamsDontOverlap(View.FOCUS_RIGHT, rect1, rect2); |
| |
| // just beyond |
| rect2.offset(0, 1); |
| assertBeamsDontOverlap(View.FOCUS_LEFT, rect1, rect2); |
| assertBeamsDontOverlap(View.FOCUS_RIGHT, rect1, rect2); |
| |
| // just below top edge |
| rect2.set(rect1); |
| rect2.offset(0, -(rect1.height() - 1)); |
| assertBeamsOverlap(View.FOCUS_LEFT, rect1, rect2); |
| assertBeamsOverlap(View.FOCUS_RIGHT, rect1, rect2); |
| |
| // at top edge |
| rect2.offset(0, -1); |
| assertBeamsDontOverlap(View.FOCUS_LEFT, rect1, rect2); |
| assertBeamsDontOverlap(View.FOCUS_RIGHT, rect1, rect2); |
| |
| // just beyond top edge |
| rect2.offset(0, -1); |
| assertBeamsDontOverlap(View.FOCUS_LEFT, rect1, rect2); |
| assertBeamsDontOverlap(View.FOCUS_RIGHT, rect1, rect2); |
| } |
| |
| @SmallTest |
| public void testOverlapBeamsUpDownUpToEdge() { |
| final Rect rect1 = new Rect(0, 0, 20, 20); |
| final Rect rect2 = new Rect(rect1); |
| |
| // just short of right edge |
| rect2.offset(rect1.width() - 1, 0); |
| assertBeamsOverlap(View.FOCUS_UP, rect1, rect2); |
| assertBeamsOverlap(View.FOCUS_DOWN, rect1, rect2); |
| |
| // at edge |
| rect2.offset(1, 0); |
| assertBeamsDontOverlap(View.FOCUS_UP, rect1, rect2); |
| assertBeamsDontOverlap(View.FOCUS_DOWN, rect1, rect2); |
| |
| // just beyond |
| rect2.offset(1, 0); |
| assertBeamsDontOverlap(View.FOCUS_UP, rect1, rect2); |
| assertBeamsDontOverlap(View.FOCUS_DOWN, rect1, rect2); |
| |
| // just short of left edge |
| rect2.set(rect1); |
| rect2.offset(-(rect1.width() - 1), 0); |
| assertBeamsOverlap(View.FOCUS_UP, rect1, rect2); |
| assertBeamsOverlap(View.FOCUS_DOWN, rect1, rect2); |
| |
| // at edge |
| rect2.offset(-1, 0); |
| assertBeamsDontOverlap(View.FOCUS_UP, rect1, rect2); |
| assertBeamsDontOverlap(View.FOCUS_DOWN, rect1, rect2); |
| |
| // just beyond edge |
| rect2.offset(-1, 0); |
| assertBeamsDontOverlap(View.FOCUS_UP, rect1, rect2); |
| assertBeamsDontOverlap(View.FOCUS_DOWN, rect1, rect2); |
| } |
| |
| @SmallTest |
| public void testDirectlyAboveTrumpsAboveLeft() { |
| Rect src = new Rect(0, 50, 20, 70); // src (left, top, right, bottom) |
| |
| Rect directlyAbove = new Rect(src); |
| directlyAbove.offset(0, -(1 + src.height())); |
| |
| Rect aboveLeft = new Rect(src); |
| aboveLeft.offset(-(1 + src.width()), -(1 + src.height())); |
| |
| assertBetterCandidate(View.FOCUS_UP, src, directlyAbove, aboveLeft); |
| } |
| |
| @SmallTest |
| public void testAboveInBeamTrumpsSlightlyCloserOutOfBeam() { |
| Rect src = new Rect(0, 50, 20, 70); // src (left, top, right, bottom) |
| |
| Rect directlyAbove = new Rect(src); |
| directlyAbove.offset(0, -(1 + src.height())); |
| |
| Rect aboveLeft = new Rect(src); |
| aboveLeft.offset(-(1 + src.width()), -(1 + src.height())); |
| |
| // offset directly above a little further up |
| directlyAbove.offset(0, -5); |
| assertBetterCandidate(View.FOCUS_UP, src, directlyAbove, aboveLeft); |
| } |
| |
| @SmallTest |
| public void testOutOfBeamBeatsInBeamUp() { |
| |
| Rect src = new Rect(0, 0, 50, 50); // (left, top, right, bottom) |
| |
| Rect aboveLeftOfBeam = new Rect(src); |
| aboveLeftOfBeam.offset(-(src.width() + 1), -src.height()); |
| assertBeamsDontOverlap(View.FOCUS_UP, src, aboveLeftOfBeam); |
| |
| Rect aboveInBeam = new Rect(src); |
| aboveInBeam.offset(0, -src.height()); |
| assertBeamsOverlap(View.FOCUS_UP, src, aboveInBeam); |
| |
| // in beam wins |
| assertBetterCandidate(View.FOCUS_UP, src, aboveInBeam, aboveLeftOfBeam); |
| |
| // still wins while aboveInBeam's bottom edge is < out of beams' top |
| aboveInBeam.offset(0, -(aboveLeftOfBeam.height() - 1)); |
| assertTrue("aboveInBeam.bottom > aboveLeftOfBeam.top", aboveInBeam.bottom > aboveLeftOfBeam.top); |
| assertBetterCandidate(View.FOCUS_UP, src, aboveInBeam, aboveLeftOfBeam); |
| |
| // cross the threshold: the out of beam prevails |
| aboveInBeam.offset(0, -1); |
| assertEquals(aboveInBeam.bottom, aboveLeftOfBeam.top); |
| assertBetterCandidate(View.FOCUS_UP, src, aboveLeftOfBeam, aboveInBeam); |
| } |
| |
| /** |
| * A non-candidate (even a much closer one) is always a worse choice |
| * than a real candidate. |
| */ |
| @SmallTest |
| public void testSomeCandidateBetterThanNonCandidate() { |
| Rect src = new Rect(0, 0, 50, 50); // (left, top, right, bottom) |
| |
| Rect nonCandidate = new Rect(src); |
| nonCandidate.offset(src.width() + 1, 0); |
| |
| assertIsNotCandidate(View.FOCUS_LEFT, src, nonCandidate); |
| |
| Rect candidate = new Rect(src); |
| candidate.offset(-(4 * src.width()), 0); |
| assertDirectionIsCandidate(View.FOCUS_LEFT, src, candidate); |
| |
| assertBetterCandidate(View.FOCUS_LEFT, src, candidate, nonCandidate); |
| } |
| |
| /** |
| * Grabbed from {@link android.widget.focus.VerticalFocusSearchTest#testSearchFromMidLeft()} |
| */ |
| @SmallTest |
| public void testVerticalFocusSearchScenario() { |
| assertBetterCandidate(View.FOCUS_DOWN, |
| // L T R B |
| new Rect(0, 109, 153, 169), // src |
| new Rect(166, 169, 319, 229), // expectedbetter |
| new Rect(0, 229, 320, 289)); // expectedworse |
| |
| // failing test 4/10/2008, the values were tweaked somehow in functional |
| // test... |
| assertBetterCandidate(View.FOCUS_DOWN, |
| // L T R B |
| new Rect(0, 91, 153, 133), // src |
| new Rect(166, 133, 319, 175), // expectedbetter |
| new Rect(0, 175, 320, 217)); // expectedworse |
| |
| } |
| |
| /** |
| * Example: going down from a thin button all the way to the left of a |
| * screen where, just below, is a very wide button, and just below that, |
| * is an equally skinny button all the way to the left. want to make |
| * sure any minor axis factor doesn't override the fact that the one below |
| * in vertical beam should be next focus |
| */ |
| @SmallTest |
| public void testBeamsOverlapMajorAxisCloserMinorAxisFurther() { |
| assertBetterCandidate(View.FOCUS_DOWN, |
| // L T R B |
| new Rect(0, 0, 100, 100), // src |
| new Rect(0, 100, 480, 200), // expectedbetter |
| new Rect(0, 200, 100, 300)); // expectedworse |
| } |
| |
| /** |
| * Real scenario grabbed from song playback screen. |
| */ |
| @SmallTest |
| public void testMusicPlaybackScenario() { |
| assertBetterCandidate(View.FOCUS_LEFT, |
| // L T R B |
| new Rect(227, 185, 312, 231), // src |
| new Rect(195, 386, 266, 438), // expectedbetter |
| new Rect(124, 386, 195, 438)); // expectedworse |
| } |
| |
| /** |
| * more generalized version of {@link #testMusicPlaybackScenario()} |
| */ |
| @SmallTest |
| public void testOutOfBeamOverlapBeatsOutOfBeamFurtherOnMajorAxis() { |
| assertBetterCandidate(View.FOCUS_DOWN, |
| // L T R B |
| new Rect(0, 0, 50, 50), // src |
| new Rect(60, 40, 110, 90), // expectedbetter |
| new Rect(60, 70, 110, 120)); // expectedworse |
| } |
| |
| /** |
| * Make sure that going down prefers views that are actually |
| * down (and not those next to but still a candidate because |
| * they are overlapping on the major axis) |
| */ |
| @SmallTest |
| public void testInBeamTrumpsOutOfBeamOverlapping() { |
| assertBetterCandidate(View.FOCUS_DOWN, |
| // L T R B |
| new Rect(0, 0, 50, 50), // src |
| new Rect(0, 60, 50, 110), // expectedbetter |
| new Rect(51, 1, 101, 51)); // expectedworse |
| } |
| |
| @SmallTest |
| public void testOverlappingBeatsNonOverlapping() { |
| assertBetterCandidate(View.FOCUS_DOWN, |
| // L T R B |
| new Rect(0, 0, 50, 50), // src |
| new Rect(0, 40, 50, 90), // expectedbetter |
| new Rect(0, 75, 50, 125)); // expectedworse |
| } |
| |
| @SmallTest |
| public void testEditContactScenarioLeftFromDiscardChangesGoesToSaveContactInLandscape() { |
| assertBetterCandidate(View.FOCUS_LEFT, |
| // L T R B |
| new Rect(357, 258, 478, 318), // src |
| new Rect(2, 258, 100, 318), // better |
| new Rect(106, 120, 424, 184)); // worse |
| } |
| |
| /** |
| * A dial pad with 9 squares arranged in a grid. no padding, so |
| * the edges are equal. see {@link android.widget.focus.LinearLayoutGrid} |
| */ |
| @SmallTest |
| public void testGridWithTouchingEdges() { |
| assertBetterCandidate(View.FOCUS_DOWN, |
| // L T R B |
| new Rect(106, 49, 212, 192), // src |
| new Rect(106, 192, 212, 335), // better |
| new Rect(0, 192, 106, 335)); // worse |
| |
| assertBetterCandidate(View.FOCUS_DOWN, |
| // L T R B |
| new Rect(106, 49, 212, 192), // src |
| new Rect(106, 192, 212, 335), // better |
| new Rect(212, 192, 318, 335)); // worse |
| } |
| |
| @SmallTest |
| public void testSearchFromEmptyRect() { |
| assertBetterCandidate(View.FOCUS_DOWN, |
| // L T R B |
| new Rect(0, 0, 0, 0), // src |
| new Rect(0, 0, 320, 45), // better |
| new Rect(0, 45, 320, 545)); // worse |
| } |
| |
| /** |
| * Reproduce bug 1124559, drilling down to actual bug |
| * (majorAxisDistance was wrong for direction left) |
| */ |
| @SmallTest |
| public void testGmailReplyButtonsScenario() { |
| assertBetterCandidate(View.FOCUS_LEFT, |
| // L T R B |
| new Rect(223, 380, 312, 417), // src |
| new Rect(102, 380, 210, 417), // better |
| new Rect(111, 443, 206, 480)); // worse |
| |
| assertBeamBeats(View.FOCUS_LEFT, |
| // L T R B |
| new Rect(223, 380, 312, 417), // src |
| new Rect(102, 380, 210, 417), // better |
| new Rect(111, 443, 206, 480)); // worse |
| |
| assertBeamsOverlap(View.FOCUS_LEFT, |
| // L T R B |
| new Rect(223, 380, 312, 417), |
| new Rect(102, 380, 210, 417)); |
| |
| assertBeamsDontOverlap(View.FOCUS_LEFT, |
| // L T R B |
| new Rect(223, 380, 312, 417), |
| new Rect(111, 443, 206, 480)); |
| |
| assertTrue( |
| "major axis distance less than major axis distance to " |
| + "far edge", |
| FocusFinderHelper.majorAxisDistance(View.FOCUS_LEFT, |
| // L T R B |
| new Rect(223, 380, 312, 417), |
| new Rect(102, 380, 210, 417)) < |
| FocusFinderHelper.majorAxisDistanceToFarEdge(View.FOCUS_LEFT, |
| // L T R B |
| new Rect(223, 380, 312, 417), |
| new Rect(111, 443, 206, 480))); |
| } |
| |
| @SmallTest |
| public void testGmailScenarioBug1203288() { |
| assertBetterCandidate(View.FOCUS_DOWN, |
| // L T R B |
| new Rect(0, 2, 480, 82), // src |
| new Rect(344, 87, 475, 124), // better |
| new Rect(0, 130, 480, 203)); // worse |
| } |
| |
| @SmallTest |
| public void testHomeShortcutScenarioBug1295354() { |
| assertBetterCandidate(View.FOCUS_RIGHT, |
| // L T R B |
| new Rect(3, 338, 77, 413), // src |
| new Rect(163, 338, 237, 413), // better |
| new Rect(83, 38, 157, 113)); // worse |
| } |
| |
| @SmallTest |
| public void testBeamAlwaysBeatsHoriz() { |
| assertBetterCandidate(View.FOCUS_RIGHT, |
| // L T R B |
| new Rect(0, 0, 50, 50), // src |
| new Rect(150, 0, 200, 50), // better, (way further, but in beam) |
| new Rect(60, 51, 110, 101)); // worse, even though it is closer |
| |
| assertBetterCandidate(View.FOCUS_LEFT, |
| // L T R B |
| new Rect(150, 0, 200, 50), // src |
| new Rect(0, 0, 50, 50), // better, (way further, but in beam) |
| new Rect(49, 99, 149, 101)); // worse, even though it is closer |
| } |
| |
| @SmallTest |
| public void testIsCandidateOverlappingEdgeFromEmptyRect() { |
| assertDirectionIsCandidate(View.FOCUS_DOWN, |
| // L T R B |
| new Rect(0, 0, 0, 0), // src |
| new Rect(0, 0, 20, 1)); // candidate |
| |
| assertDirectionIsCandidate(View.FOCUS_UP, |
| // L T R B |
| new Rect(0, 0, 0, 0), // src |
| new Rect(0, -1, 20, 0)); // candidate |
| |
| assertDirectionIsCandidate(View.FOCUS_LEFT, |
| // L T R B |
| new Rect(0, 0, 0, 0), // src |
| new Rect(-1, 0, 0, 20)); // candidate |
| |
| assertDirectionIsCandidate(View.FOCUS_RIGHT, |
| // L T R B |
| new Rect(0, 0, 0, 0), // src |
| new Rect(0, 0, 1, 20)); // candidate |
| } |
| |
| private void assertBeamsOverlap(int direction, Rect rect1, Rect rect2) { |
| String directionStr = validateAndGetStringFor(direction); |
| String assertMsg = String.format("Expected beams to overlap in direction %s " |
| + "for rectangles %s and %s", directionStr, rect1, rect2); |
| assertTrue(assertMsg, mFocusFinder.beamsOverlap(direction, rect1, rect2)); |
| } |
| |
| private void assertBeamsDontOverlap(int direction, Rect rect1, Rect rect2) { |
| String directionStr = validateAndGetStringFor(direction); |
| String assertMsg = String.format("Expected beams not to overlap in direction %s " |
| + "for rectangles %s and %s", directionStr, rect1, rect2); |
| assertFalse(assertMsg, mFocusFinder.beamsOverlap(direction, rect1, rect2)); |
| } |
| |
| /** |
| * Assert that particular rect is a better focus search candidate from a |
| * source rect than another. |
| * @param direction The direction of focus search. |
| * @param srcRect The src rectangle. |
| * @param expectedBetter The candidate that should be better. |
| * @param expectedWorse The candidate that should be worse. |
| */ |
| private void assertBetterCandidate(int direction, Rect srcRect, |
| Rect expectedBetter, Rect expectedWorse) { |
| |
| String directionStr = validateAndGetStringFor(direction); |
| String assertMsg = String.format( |
| "expected %s to be a better focus search candidate than " |
| + "%s when searching " |
| + "from %s in direction %s", |
| expectedBetter, expectedWorse, srcRect, directionStr); |
| |
| assertTrue(assertMsg, |
| mFocusFinder.isBetterCandidate(direction, srcRect, |
| expectedBetter, expectedWorse)); |
| |
| assertMsg = String.format( |
| "expected %s to not be a better focus search candidate than " |
| + "%s when searching " |
| + "from %s in direction %s", |
| expectedWorse, expectedBetter, srcRect, directionStr); |
| |
| assertFalse(assertMsg, |
| mFocusFinder.isBetterCandidate(direction, srcRect, |
| expectedWorse, expectedBetter)); |
| } |
| |
| private void assertIsNotCandidate(int direction, Rect src, Rect dest) { |
| String directionStr = validateAndGetStringFor(direction); |
| |
| final String assertMsg = String.format( |
| "expected going from %s to %s in direction %s to be an invalid " |
| + "focus search candidate", |
| src, dest, directionStr); |
| assertFalse(assertMsg, mFocusFinder.isCandidate(src, dest, direction)); |
| } |
| |
| private void assertBeamBeats(int direction, Rect srcRect, |
| Rect rect1, Rect rect2) { |
| |
| String directionStr = validateAndGetStringFor(direction); |
| String assertMsg = String.format( |
| "expecting %s to beam beat %s w.r.t %s in direction %s", |
| rect1, rect2, srcRect, directionStr); |
| assertTrue(assertMsg, mFocusFinder.beamBeats(direction, srcRect, rect1, rect2)); |
| } |
| |
| |
| private void assertDirectionIsCandidate(int direction, Rect src, Rect dest) { |
| String directionStr = validateAndGetStringFor(direction); |
| |
| final String assertMsg = String.format( |
| "expected going from %s to %s in direction %s to be a valid " |
| + "focus search candidate", |
| src, dest, directionStr); |
| assertTrue(assertMsg, mFocusFinder.isCandidate(src, dest, direction)); |
| } |
| |
| private String validateAndGetStringFor(int direction) { |
| String directionStr = "??"; |
| switch(direction) { |
| case View.FOCUS_UP: |
| directionStr = "FOCUS_UP"; |
| break; |
| case View.FOCUS_DOWN: |
| directionStr = "FOCUS_DOWN"; |
| break; |
| case View.FOCUS_LEFT: |
| directionStr = "FOCUS_LEFT"; |
| break; |
| case View.FOCUS_RIGHT: |
| directionStr = "FOCUS_RIGHT"; |
| break; |
| default: |
| fail("passed in unknown direction, ya blewit!"); |
| } |
| return directionStr; |
| } |
| |
| |
| } |