This article undertakes Android finds the nearest control that can get focus (I)
findNextFocus() step 3
The relevant codes are excerpted as follows:
focusables.clear(); effectiveRoot.addFocusables(focusables, direction); if (!focusables.isEmpty()) { next = findNextFocus(effectiveRoot, focused, focusedRect, direction, focusables); }
First, add all the controls in the effectiveRoot that can obtain the focus to the focusables queue. Then, when focusables is not empty, execute the overloaded method findNextFocus(effectiveRoot, focused, focusedRect, direction, focusables) to obtain the next control according to the navigation direction.
effectiveRoot.addFocusables(focusables, direction)
let's take a look at adding a control that can get focus to focusables:
public void addFocusables(ArrayList<View> views, @FocusDirection int direction) { addFocusables(views, direction, isInTouchMode() ? FOCUSABLES_TOUCH_MODE : FOCUSABLES_ALL); }
First judge whether it is in touch mode. If yes, the parameter is FOCUSABLES_TOUCH_MODE, if not touch mode, the parameter is FOCUSABLES_ALL, and then call another overloaded function. This overloaded function calls different implementations according to whether the caller is of View type or ViewGroup type. First look at the of View class:
public void addFocusables(ArrayList<View> views, @FocusDirection int direction, @FocusableMode int focusableMode) { if (views == null) { return; } if (!canTakeFocus()) { return; } if ((focusableMode & FOCUSABLES_TOUCH_MODE) == FOCUSABLES_TOUCH_MODE && !isFocusableInTouchMode()) { return; } views.add(this); }
This method first checks that the parameter views cannot be null, and then checks that the control can obtain the focus. If one cannot be, it will return directly. Then check whether the parameter focusableMode is set to focusables_ TOUCH_ The mode ID, if set, indicates that the control is in touch mode. In this case, if the control is not isFocusableInTouchMode(), it will be returned directly and cannot be added to the parameter views. If the above check passes, add this control to views.
take another look at addFocusables(ArrayList views, int direction, int focusableMode) of ViewGroup class:
@Override public void addFocusables(ArrayList<View> views, int direction, int focusableMode) { final int focusableCount = views.size(); final int descendantFocusability = getDescendantFocusability(); final boolean blockFocusForTouchscreen = shouldBlockFocusForTouchscreen(); final boolean focusSelf = (isFocusableInTouchMode() || !blockFocusForTouchscreen); if (descendantFocusability == FOCUS_BLOCK_DESCENDANTS) { if (focusSelf) { super.addFocusables(views, direction, focusableMode); } return; } if (blockFocusForTouchscreen) { focusableMode |= FOCUSABLES_TOUCH_MODE; } if ((descendantFocusability == FOCUS_BEFORE_DESCENDANTS) && focusSelf) { super.addFocusables(views, direction, focusableMode); } int count = 0; final View[] children = new View[mChildrenCount]; for (int i = 0; i < mChildrenCount; ++i) { View child = mChildren[i]; if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) { children[count++] = child; } } FocusFinder.sort(children, 0, count, this, isLayoutRtl()); for (int i = 0; i < count; ++i) { children[i].addFocusables(views, direction, focusableMode); } // When set to FOCUS_AFTER_DESCENDANTS, we only add ourselves if // there aren't any focusable descendants. this is // to avoid the focus search finding layouts when a more precise search // among the focusable children would be more interesting. if ((descendantFocusability == FOCUS_AFTER_DESCENDANTS) && focusSelf && focusableCount == views.size()) { super.addFocusables(views, direction, focusableMode); } }
This method mainly does the following:
1. Record some statuses, the size of the parameter views, the relationship between the current container and its subclasses to obtain the focus (descendantFocusability), whether to prevent the acquisition of the focus (blockFocusForTouchscreen) in the touch mode, and whether the container itself can obtain the focus (focusSelf).
2. When the container prevents the child control from obtaining the focus, and the container itself can obtain the focus, call addFocusables(views, direction, focusableMode) of the parent View to check whether it can be added to views. In this case, exit and do not execute downward.
3. If the focus acquisition should be blocked in the touch mode, add focusables to the parameter focusableMode_ TOUCH_ Mode identification.
4. If it is set to obtain the focus before the descendant control and the container itself can obtain the focus, addFocusables(views, direction, focusableMode) of the parent class View is called at this time.
5. Find the visible child control of the container, put it in the array children, and then sort it (call FocusFinder.sort()).
6. Recursively call the addFocusables(views, direction, focusableMode) method for each sorted child control.
7. If the container is set to obtain the focus after the descendant control, and it can obtain the focus itself, and the number after adding the control is the same as that recorded in step 1 (in fact, the container has no descendant control that can obtain the focus), then call addFocusables(views, direction, focusableMode) of the parent class View.
you can know focus from this method_ BLOCK_ DESCENDANTS,FOCUS_BEFORE_DESCENDANTS,FOCUS_ AFTER_ How are scenarios used.
shouldBlockFocusForTouchscreen() method Android controls get focus As mentioned earlier, super.addFocusables(views, direction, focusableMode) also talked about the sorting of child controls, which is FocusFinder.sort(children, 0, count, this, isLayoutRtl()):
public static void sort(View[] views, int start, int end, ViewGroup root, boolean isRtl) { getInstance().mFocusSorter.sort(views, start, end, root, isRtl); } public void sort(View[] views, int start, int end, ViewGroup root, boolean isRtl) { int count = end - start; if (count < 2) { return; } if (mRectByView == null) { mRectByView = new HashMap<>(); } mRtlMult = isRtl ? -1 : 1; for (int i = mRectPool.size(); i < count; ++i) { mRectPool.add(new Rect()); } for (int i = start; i < end; ++i) { Rect next = mRectPool.get(mLastPoolRect++); views[i].getDrawingRect(next); root.offsetDescendantRectToMyCoords(views[i], next); mRectByView.put(views[i], next); } // Sort top-to-bottom Arrays.sort(views, start, count, mTopsComparator); // Sweep top-to-bottom to identify rows int sweepBottom = mRectByView.get(views[start]).bottom; int rowStart = start; int sweepIdx = start + 1; for (; sweepIdx < end; ++sweepIdx) { Rect currRect = mRectByView.get(views[sweepIdx]); if (currRect.top >= sweepBottom) { // Next view is on a new row, sort the row we've just finished left-to-right. if ((sweepIdx - rowStart) > 1) { Arrays.sort(views, rowStart, sweepIdx, mSidesComparator); } sweepBottom = currRect.bottom; rowStart = sweepIdx; } else { // Next view vertically overlaps, we need to extend our "row height" sweepBottom = Math.max(sweepBottom, currRect.bottom); } } // Sort whatever's left (final row) left-to-right if ((sweepIdx - rowStart) > 1) { Arrays.sort(views, rowStart, sweepIdx, mSidesComparator); } mLastPoolRect = 0; mRectByView.clear(); }
Through views[i].getDrawingRect(next), put the upper, lower, left and right positions of the visible border of the control into a Rect type object, and then call root.offsetDescendantRectToMyCoords(views[i], next) to convert the coordinates of the visible border position of the child control into the coordinates of the root container. Take a look at getDrawingRect(Rect outRect) of the View class:
/** * Return the visible drawing bounds of your view. Fills in the output * rectangle with the values from getScrollX(), getScrollY(), * getWidth(), and getHeight(). These bounds do not account for any * transformation properties currently set on the view, such as * {@link #setScaleX(float)} or {@link #setRotation(float)}. * * @param outRect The (scrolled) drawing bounds of the view. */ public void getDrawingRect(Rect outRect) { outRect.left = mScrollX; outRect.top = mScrollY; outRect.right = mScrollX + (mRight - mLeft); outRect.bottom = mScrollY + (mBottom - mTop); }
The position coordinates of the visible border need to be added with mscollx and mscolly respectively. Mscollx and mscolly are equivalent to offsetting the coordinate axis of the control. And through the annotation, we know that the border will not change. When transforming attributes, such as setting scaling or rotation.
look at the code offsetdescendantrecttomycords (view descendant, rect rect) for coordinate conversion to the parent control:
public final void offsetDescendantRectToMyCoords(View descendant, Rect rect) { offsetRectBetweenParentAndChild(descendant, rect, true, false); } void offsetRectBetweenParentAndChild(View descendant, Rect rect, boolean offsetFromChildToParent, boolean clipToBounds) { // already in the same coord system :) if (descendant == this) { return; } ViewParent theParent = descendant.mParent; // search and offset up to the parent while ((theParent != null) && (theParent instanceof View) && (theParent != this)) { if (offsetFromChildToParent) { rect.offset(descendant.mLeft - descendant.mScrollX, descendant.mTop - descendant.mScrollY); if (clipToBounds) { View p = (View) theParent; boolean intersected = rect.intersect(0, 0, p.mRight - p.mLeft, p.mBottom - p.mTop); if (!intersected) { rect.setEmpty(); } } } else { if (clipToBounds) { View p = (View) theParent; boolean intersected = rect.intersect(0, 0, p.mRight - p.mLeft, p.mBottom - p.mTop); if (!intersected) { rect.setEmpty(); } } rect.offset(descendant.mScrollX - descendant.mLeft, descendant.mScrollY - descendant.mTop); } descendant = (View) theParent; theParent = descendant.mParent; } // now that we are up to this view, need to offset one more time // to get into our coordinate space if (theParent == this) { if (offsetFromChildToParent) { rect.offset(descendant.mLeft - descendant.mScrollX, descendant.mTop - descendant.mScrollY); } else { rect.offset(descendant.mScrollX - descendant.mLeft, descendant.mScrollY - descendant.mTop); } } else { throw new IllegalArgumentException("parameter must be a descendant of this view"); } }
It can be seen that rect.offset (descendant.mleft - descendant.mscollx, descendant.mtop - descendant.mscolly) is called to obtain the coordinates in the parent control (this coordinate value is the mscollx or mscolly of the parent control). Therefore, when reaching the parent control of the parent control, the mscollx or mscolly of the parent control is subtracted. In this way, when you cycle to the top, you get the coordinate position of the border of the focus control in the top control.
mTopsComparator is a Comparator used for sorting. It is arranged in ascending order according to the top size of the control. If it is equal, it is arranged in ascending order according to the bottom size. mSidesComparator is also a Comparator. It is arranged in ascending or descending order according to whether the layout is left to right or right to left. If the layout is left to right, it is first arranged in ascending order according to the left size of the control. If it is equal, it is then arranged in ascending order according to the right size. If the layout is right to left, it is in descending order.
look at the code. First, sort it according to mTopsComparator. At this time, the views are arranged in ascending order according to the top size. If they are equal, they are arranged in ascending order according to the bottom size. Then, another loop is used for mSidesComparator sorting. What does this code mean? This is to sort the mSidesComparator for the upper and lower overlapping controls. Find the first control that does not intersect with the previous control (that is, the condition currrect. Top > = sweepbottom) in the order in the views at this time. If the number of controls before the found control exceeds 1 ((sweepidx - rowstart) > 1), you need to sort these controls by mSidesComparator. Then, find the second control that does not intersect with the previous control. If the number of price controls between the second and the first (excluding the second control) exceeds 1, also sort by mSidesComparator. Until the end of the cycle. If (sweepidx - rowstart) > 1, it indicates that there are several upper and lower intersecting controls at the end, which are also sorted by mSidesComparator.
therefore, after the final sorting is completed, the result is divided into several segments according to the disjoint of top and bottom. The members between each segment are sorted by mSidesComparator.
in this way, the method in effectiveRoot.addFocusables(focusables, direction) is analyzed.
the following points can be seen:
1. Focus is set in the container_ BLOCK_ If the container can obtain the focus, only the container itself will be added in the case of scenarios ID; If the container can't get focus, nothing will be added.
2. Focus is set in the container_ BEFORE_ If the container can get the focus, the container is added to the collection of focus getting controls before its child controls.
3. The order of adding child controls is divided into several segments according to the disjoint of top and bottom. The members between each segment are sorted by mSidesComparator.
4. Focus is set in the container_ AFTER_ Under the scenarios ID, container controls can be added only when none of the child controls can get the focus.
look again Step 3 Overloaded method findNextFocus(effectiveRoot, focused, focusedRect, direction, focusables)
findNextFocus(effectiveRoot, focused, focusedRect, direction, focusables)
private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction, ArrayList<View> focusables) { if (focused != null) { if (focusedRect == null) { focusedRect = mFocusedRect; } // fill in interesting rect from focused focused.getFocusedRect(focusedRect); root.offsetDescendantRectToMyCoords(focused, focusedRect); } else { if (focusedRect == null) { focusedRect = mFocusedRect; // make up a rect at top left or bottom right of root switch (direction) { case View.FOCUS_RIGHT: case View.FOCUS_DOWN: setFocusTopLeft(root, focusedRect); break; case View.FOCUS_FORWARD: if (root.isLayoutRtl()) { setFocusBottomRight(root, focusedRect); } else { setFocusTopLeft(root, focusedRect); } break; case View.FOCUS_LEFT: case View.FOCUS_UP: setFocusBottomRight(root, focusedRect); break; case View.FOCUS_BACKWARD: if (root.isLayoutRtl()) { setFocusTopLeft(root, focusedRect); } else { setFocusBottomRight(root, focusedRect); break; } } } } switch (direction) { case View.FOCUS_FORWARD: case View.FOCUS_BACKWARD: return findNextFocusInRelativeDirection(focusables, root, focused, focusedRect, direction); case View.FOCUS_UP: case View.FOCUS_DOWN: case View.FOCUS_LEFT: case View.FOCUS_RIGHT: return findNextFocusInAbsoluteDirection(focusables, root, focused, focusedRect, direction); default: throw new IllegalArgumentException("Unknown direction: " + direction); } }
The code of this method is divided into two parts. The above if else is to assign a value to focusedRect, and the following switch case is to find the control in the corresponding direction.
focusedRect assignment section
first look at the part of assigning value to focusedRect. The parameter focused is the control to obtain focus. If it is not empty, the border position of focused is converted to the coordinate value of the root control.
if focused is empty, set focusedRect according to the value of the direction. Take a look at setFocusBottomRight(ViewGroup root, Rect focusedRect), setFocusTopLeft(ViewGroup root, Rect focusedRect):
private void setFocusBottomRight(ViewGroup root, Rect focusedRect) { final int rootBottom = root.getScrollY() + root.getHeight(); final int rootRight = root.getScrollX() + root.getWidth(); focusedRect.set(rootRight, rootBottom, rootRight, rootBottom); } private void setFocusTopLeft(ViewGroup root, Rect focusedRect) { final int rootTop = root.getScrollY(); final int rootLeft = root.getScrollX(); focusedRect.set(rootLeft, rootTop, rootLeft, rootTop); }
Setfocusbottomlight (ViewGroup root, rect, focusedRect) is the upper left corner of the border of the control that sets focusedRect as root, and setfocusbottomlight () is the lower right corner of the border of the control that sets ocusedRect as root. In this way, we can get the different areas set in each direction according to the code.
Find the control in the corresponding direction
It can be seen that two methods are called according to different directions. Let's first look at the setting of FOCUS_UP,FOCUS_DOWN,FOCUS_LEFT,FOCUS_ Method findnextfocusinabsolutedirection called by right (focusables, root, focused, focusedrect, direction):
View findNextFocusInAbsoluteDirection(ArrayList<View> focusables, ViewGroup root, View focused, Rect focusedRect, int direction) { // initialize the best candidate to something impossible // (so the first plausible view will become the best choice) mBestCandidateRect.set(focusedRect); switch(direction) { case View.FOCUS_LEFT: mBestCandidateRect.offset(focusedRect.width() + 1, 0); break; case View.FOCUS_RIGHT: mBestCandidateRect.offset(-(focusedRect.width() + 1), 0); break; case View.FOCUS_UP: mBestCandidateRect.offset(0, focusedRect.height() + 1); break; case View.FOCUS_DOWN: mBestCandidateRect.offset(0, -(focusedRect.height() + 1)); } View closest = null; int numFocusables = focusables.size(); for (int i = 0; i < numFocusables; i++) { View focusable = focusables.get(i); // only interested in other non-root views if (focusable == focused || focusable == root) continue; // get focus bounds of other view in same coordinate system focusable.getFocusedRect(mOtherRect); root.offsetDescendantRectToMyCoords(focusable, mOtherRect); if (isBetterCandidate(direction, focusedRect, mOtherRect, mBestCandidateRect)) { mBestCandidateRect.set(mOtherRect); closest = focusable; } } return closest; }
First, set the best rectangle mbestcandidaterrect, but the best matrix is initially set to an unlikely position, so it can be removed by comparison later. Then, through a loop, compare the border positions of all the controls in focusables that can obtain the focus with the best position, mbestcandiderect. The control in the best position compared by the loop is the control to be found.
let's see how to set mbestcandiderect to an impossible position at the beginning. First, set mbestcandiderect to focusedRect, which is the position to get the border of the focus control (when focused is not null). In the direction is FOCUS_LEFT, mbestcandiderect. Offset (focusedRect. Width() + 1, 0). Originally, it was to find the control on the left. As a result, mbestcandiderect was offset to the right by a distance of control width + 1. So the control you are looking for may not be on this side. The same is true when looking in other directions.
next, let's look at how to compare. We mainly call the isbettercandidate (int direction, rect source, rect rect1, rect2) method:
boolean isBetterCandidate(int direction, Rect source, Rect rect1, Rect rect2) { // to be a better candidate, need to at least be a candidate in the first // place :) if (!isCandidate(source, rect1, direction)) { return false; } // we know that rect1 is a candidate.. if rect2 is not a candidate, // rect1 is better if (!isCandidate(source, rect2, direction)) { return true; } // if rect1 is better by beam, it wins if (beamBeats(direction, source, rect1, rect2)) { return true; } // if rect2 is better, then rect1 cant' be :) if (beamBeats(direction, source, rect2, rect1)) { return false; } // otherwise, do fudge-tastic comparison of the major and minor axis return (getWeightedDistanceFor( majorAxisDistance(direction, source, rect1), minorAxisDistance(direction, source, rect1)) < getWeightedDistanceFor( majorAxisDistance(direction, source, rect2), minorAxisDistance(direction, source, rect2))); }
This method mainly calls isCandidate(), beamBeats() method. First look at these two methods:
isCandidate(Rect srcRect, Rect destRect, int direction):
boolean isCandidate(Rect srcRect, Rect destRect, int direction) { switch (direction) { case View.FOCUS_LEFT: return (srcRect.right > destRect.right || srcRect.left >= destRect.right) && srcRect.left > destRect.left; case View.FOCUS_RIGHT: return (srcRect.left < destRect.left || srcRect.right <= destRect.left) && srcRect.right < destRect.right; case View.FOCUS_UP: return (srcRect.bottom > destRect.bottom || srcRect.top >= destRect.bottom) && srcRect.top > destRect.top; case View.FOCUS_DOWN: return (srcRect.top < destRect.top || srcRect.bottom <= destRect.top) && srcRect.bottom < destRect.bottom; } throw new IllegalArgumentException("direction must be one of " + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}."); }
Iscandidate (rect, srcRect, rect, destRect, int direction) is mainly used for comparison according to different search directions. Like focus_ In the left direction, the left boundary of destRect must be to the left of the left boundary of srcRect, and the right boundary of destRect must be to the left of the right boundary of srcRect. However, the right boundary of destRect and the left boundary of srcRect can overlap. The same is true for other directions and self analysis.
take another look at beambeats (int direction, rect source, rect rect1, rect2):
boolean beamBeats(int direction, Rect source, Rect rect1, Rect rect2) { final boolean rect1InSrcBeam = beamsOverlap(direction, source, rect1); final boolean rect2InSrcBeam = beamsOverlap(direction, source, rect2); // if rect1 isn't exclusively in the src beam, it doesn't win if (rect2InSrcBeam || !rect1InSrcBeam) { return false; } // we know rect1 is in the beam, and rect2 is not // if rect1 is to the direction of, and rect2 is not, rect1 wins. // for example, for direction left, if rect1 is to the left of the source // and rect2 is below, then we always prefer the in beam rect1, since rect2 // could be reached by going down. if (!isToDirectionOf(direction, source, rect2)) { return true; } // for horizontal directions, being exclusively in beam always wins if ((direction == View.FOCUS_LEFT || direction == View.FOCUS_RIGHT)) { return true; } // for vertical directions, beams only beat up to a point: // now, as long as rect2 isn't completely closer, rect1 wins // e.g for direction down, completely closer means for rect2's top // edge to be closer to the source's top edge than rect1's bottom edge. return (majorAxisDistance(direction, source, rect1) < majorAxisDistanceToFarEdge(direction, source, rect2)); }
The beamsovlap () method is used to determine whether there is intersection in another direction according to the search direction.
boolean beamsOverlap(int direction, Rect rect1, Rect rect2) { switch (direction) { case View.FOCUS_LEFT: case View.FOCUS_RIGHT: return (rect2.bottom > rect1.top) && (rect2.top < rect1.bottom); case View.FOCUS_UP: case View.FOCUS_DOWN: return (rect2.right > rect1.left) && (rect2.left < rect1.right); } throw new IllegalArgumentException("direction must be one of " + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}."); }
If the search direction is FOCUS_LEFT or FOCUS_RIGHT, it depends on whether there is intersection in the vertical direction; If the direction is FOCUS_UP or FOCUS_DOWN, it depends on whether there is intersection in the horizontal direction.
beamBeats() if (rect2insrcbeam |! Rect1insrcbeam), it indicates that rect2 intersects with the focus control or rect1 does not intersect. false is returned and subsequent comparison is required. Istodictionof (direction, source, rect2) is used to determine whether rect2 is completely located on the side of the source search direction according to the search direction. For example, find direction FOCUS_LEFT is to judge whether rect2 is completely located to the left of the source. To reverse is to judge whether rect2 is completely on the left side of the source. If not, return true. Represents that rect1 is more suitable than rect2.
beamBeats() then judges downward if the search direction is FOCUS_LEFT or FOCUS_RIGHT, also returns true, indicating that rect1 is better than rect2. If none of the above is satisfied and the direction is FOCUS_UP or FOCUS_DOWN, the judgment will be made next. Take a look at majorAxisDistance(direction, source, rect1):
static int majorAxisDistance(int direction, Rect source, Rect dest) { return Math.max(0, majorAxisDistanceRaw(direction, source, dest)); } static int majorAxisDistanceRaw(int direction, Rect source, Rect dest) { switch (direction) { case View.FOCUS_LEFT: return source.left - dest.right; case View.FOCUS_RIGHT: return dest.left - source.right; case View.FOCUS_UP: return source.top - dest.bottom; case View.FOCUS_DOWN: return dest.top - source.bottom; } throw new IllegalArgumentException("direction must be one of " + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}."); }
The main focus is FOCUS_UP or FOCUS_DOWN, if FOCUS_UP, the result is the distance from the top edge of source to the bottom edge of rect1. If it is less than 0, take 0. If FOCUS_DOWN, the result is the distance from the top edge of rect1 to the bottom edge of source. If it is less than 0, take 0.
take a look at majorAxisDistanceToFarEdge(direction, source, rect2):
static int majorAxisDistanceToFarEdge(int direction, Rect source, Rect dest) { return Math.max(1, majorAxisDistanceToFarEdgeRaw(direction, source, dest)); } static int majorAxisDistanceToFarEdgeRaw(int direction, Rect source, Rect dest) { switch (direction) { case View.FOCUS_LEFT: return source.left - dest.left; case View.FOCUS_RIGHT: return dest.right - source.right; case View.FOCUS_UP: return source.top - dest.top; case View.FOCUS_DOWN: return dest.bottom - source.bottom; } throw new IllegalArgumentException("direction must be one of " + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}."); }
The main focus is FOCUS_UP or FOCUS_DOWN, if FOCUS_UP, the result is the distance from the top edge of source to the top edge of rect2. If it is less than 1, take 1. If FOCUS_DOWN, the result is the distance from the bottom edge of rect2 to the bottom edge of source. If it is less than 1, take 1.
compare, if FOCUS_DOWN, the distance from the top edge of rect1 to the bottom edge of source (if greater than 0) is less than the distance from the bottom edge of rect2 to the bottom edge of source (if greater than 1), then true is returned; if not, false is returned.
compare, if FOCUS_UP, then the distance from the top edge of source to the bottom edge of rect1 (if greater than 0) is less than the distance from the top edge of source to the top edge of rect2 (if greater than 1), then true is returned. If not, false is returned.
now you can analyze the logic of isBetterCandidate().
isBetterCandidate() calls isCandidate() twice, but the parameters passed are different. The first pass is the position of the control to be compared. In this way, if the first comparison isCandidate() does not meet the conditions, it means that the control to be compared does not meet the conditions (like FOCUS_LEFT, which is not on the left side of the current focus (can be crossed)). At this time, there is no need to compare backward, It directly returns false. If it meets the conditions, isCandidate() will be performed for the second time. The parameter passed in this time is the best position before. If false is returned for the second time, it indicates that it is not satisfied, but the control to be compared is satisfied. At this time, a better choice is found and true is returned. If both conditions are met, the following comparison will continue.
isBetterCandidate() called beamBeats() twice, but the last two parameters passed are different. Here rect1 represents the control to be compared, and rect2 represents the best control found in the previous comparison. For the first time, rect1 comes first and rect2 comes later. This time, if you call beamBeats(direction, source, rect1, rect2) and return true, it means that rect1 is better than rect2 and returns true directly. In the second call, rect2 comes first and rect1 comes later. If this call beamBeats(direction, source, rect2, rect1) returns true, it means that rect2 is better than rect1, so it returns false.
isBetterCandidate() will make the final comparison if the result has not been compared up to now.
take a look at the algorithm getWeightedDistanceFor(long majorAxisDistance, long minorAxisDistance):
/** * Fudge-factor opportunity: how to calculate distance given major and minor * axis distances. Warning: this fudge factor is finely tuned, be sure to * run all focus tests if you dare tweak it. */ long getWeightedDistanceFor(long majorAxisDistance, long minorAxisDistance) { return 13 * majorAxisDistance * majorAxisDistance + minorAxisDistance * minorAxisDistance; }
This method gives a way to calculate the distance between the principal axis distance (parameter majorAxisDistance) and the minor axis distance (minorAxisDistance). This calculation formula is in the code, of which 13 may be the fuzzy factor mentioned in the note, and it is also mentioned in the note that the fuzzy factor has been adjusted. If you dare to adjust it again, make sure to do a good test.
let's look at the calculation method of main axis distance and secondary axis distance:
the principal axis distance from rect1 to source is majorAxisDistance(direction, source, rect1). As mentioned above, it calculates the distances of different frames on the corresponding direction axis according to the corresponding direction. For example, the direction is FOCUS_LEFT is the horizontal direction. When calculating the distance, use the distance from the left border of source to the right border of rect1.
the secondary axis distance from rect1 to source is minorAxisDistance(direction, source, rect1)),
static int minorAxisDistance(int direction, Rect source, Rect dest) { switch (direction) { case View.FOCUS_LEFT: case View.FOCUS_RIGHT: // the distance between the center verticals return Math.abs( ((source.top + source.height() / 2) - ((dest.top + dest.height() / 2)))); case View.FOCUS_UP: case View.FOCUS_DOWN: // the distance between the center horizontals return Math.abs( ((source.left + source.width() / 2) - ((dest.left + dest.width() / 2)))); } throw new IllegalArgumentException("direction must be one of " + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}."); }
the secondary axis is opposite to the main shaft. If the main shaft is horizontal, the secondary axis is up and down. If the main axis is up and down, the secondary axis is horizontal. The secondary axis is the calculation method of the up-down direction and is the absolute value of the vertical distance from the center point of source to the center point of rect1. The secondary axis is the calculation method of the horizontal direction and the absolute value of the horizontal distance from the center point of source to the center point of rect1.
the calculation method of the main axis distance and secondary axis distance from rect2 to source is the same as the above analysis.
after analyzing the above, you can calculate the calculation method of the last step of isBetterCandidate. Call getWeightedDistanceFor(long majorAxisDistance, long minorAxisDistance) to get the correlation value of the control to be compared and the correlation value of the previous most suitable control. If the former is less than the latter, it is considered that the former is better than the latter; Otherwise, the latter is considered better.
Find controls in relative directions
the search direction is FOCUS_FORWARD or focus_ During backward, the findNextFocusInRelativeDirection(ArrayList focusables, ViewGroup root, View focused, Rect focusedRect, int direction) method is called:
private View findNextFocusInRelativeDirection(ArrayList<View> focusables, ViewGroup root, View focused, Rect focusedRect, int direction) { try { // Note: This sort is stable. mUserSpecifiedFocusComparator.setFocusables(focusables, root); Collections.sort(focusables, mUserSpecifiedFocusComparator); } finally { mUserSpecifiedFocusComparator.recycle(); } final int count = focusables.size(); switch (direction) { case View.FOCUS_FORWARD: return getNextFocusable(focused, focusables, count); case View.FOCUS_BACKWARD: return getPreviousFocusable(focused, focusables, count); } return focusables.get(count - 1); }
It can be seen that the focusables set is sorted first, and then focus is selected according to the corresponding direction_ Forward, call getNextFocusable(focused, focusables, count); If the corresponding direction is FOCUS_BACKWARD, call getPreviousFocusable(focused, focusables, count) method.
first, let's sort the focusables collection. The sorting uses muserspecificedfocuscomparator, which is the UserSpecifiedFocusComparator object. Take a look at its sorting implementation. First, take a look at setFocusables(List focusables, View root):
public void setFocusables(List<View> focusables, View root) { mRoot = root; for (int i = 0; i < focusables.size(); ++i) { mOriginalOrdinal.put(focusables.get(i), i); } for (int i = focusables.size() - 1; i >= 0; i--) { final View view = focusables.get(i); final View next = mNextFocusGetter.get(mRoot, view); if (next != null && mOriginalOrdinal.containsKey(next)) { mNextFoci.put(view, next); mIsConnectedTo.add(next); } } for (int i = focusables.size() - 1; i >= 0; i--) { final View view = focusables.get(i); final View next = mNextFoci.get(view); if (next != null && !mIsConnectedTo.contains(view)) { setHeadOfChain(view); } } } private void setHeadOfChain(View head) { for (View view = head; view != null; view = mNextFoci.get(view)) { final View otherHead = mHeadsOfChains.get(view); if (otherHead != null) { if (otherHead == head) { return; // This view has already had its head set properly } // A hydra -- multi-headed focus chain (e.g. A->C and B->C) // Use the one we've already chosen instead and reset this chain. view = head; head = otherHead; } mHeadsOfChains.put(view, head); } }
The first for loop adds all the controls in focusables to the originalordinal (ArrayMap object). The key in the originalordinal is the View value, and the value is the corresponding sequence number in focusables.
the View control can set the search direction to focus through the nextFocusForward property in the layout file_ id of the next control in forward. If the next control still has this property, it can form a chain. For example, a control nextFocusForward is the id of B control, and B control nextFocusForward is the id of C control. This chain is a - > b - > C. The second and third for loops are to obtain the source control of the control on the chain. The values are stored in mHeadsOfChains (ArrayMap object). Like the chain a - > b - > C just now, you can get mHeadsOfChains.get(A)=A, mHeadsOfChains.get(B)=A, mHeadsOfChains.get(C) = A. However, B must exist in focusables, and no other control chain in focusables points to A. We know that focusables is a control that can obtain focus contained in the root container. For details, please refer to front Therefore, B must be the descendant control that can obtain the focus in the root container, and the nextFocusForward of the descendant control that can obtain the focus in the root container cannot be A(A must be the beginning of the chain).
there is another case, that is, a - > C and B - > C in the example given in the code comments. In this case, the final situation of mHeadsOfChains is mHeadsOfChains.get(A)=A, mHeadsOfChains.get(B)=A,mHeadsOfChains.get(C) = A. follow the code to get this result. For example, when the nextFocusForward of two controls points to the same control, after the second and third for loops, the source of the control pointed to is the control pointing to it in the focusables, and the source of the control pointing to it in the top of the sorting becomes the control pointing to it in the top of the sorting Pieces.
the above code is what I said. You can go through the process according to the code. The source of these controls on the chain is obtained for the following sorting. See the sorting Code:
public int compare(View first, View second) { if (first == second) { return 0; } // Order between views within a chain is immaterial -- next/previous is // within a chain is handled elsewhere. View firstHead = mHeadsOfChains.get(first); View secondHead = mHeadsOfChains.get(second); if (firstHead == secondHead && firstHead != null) { if (first == firstHead) { return -1; // first is the head, it should be first } else if (second == firstHead) { return 1; // second is the head, it should be first } else if (mNextFoci.get(first) != null) { return -1; // first is not the end of the chain } else { return 1; // first is end of chain } } boolean involvesChain = false; if (firstHead != null) { first = firstHead; involvesChain = true; } if (secondHead != null) { second = secondHead; involvesChain = true; } if (involvesChain) { // keep original order between chains return mOriginalOrdinal.get(first) < mOriginalOrdinal.get(second) ? -1 : 1; } else { return 0; } }
when sorting, first judge that the sources of the two compared controls are equal and not null (controls in the same chain),
1. If the first control is equal to its source control, the first control is in front (corresponding situation: the first control is the beginning of the chain)
if 2 and 1 are not satisfied, and the second control is equal to its source file, the two controls compared will be exchanged (corresponding situation: the second control is the beginning of the chain).
if 3, 1 and 2 are not satisfied, check that the first control sets the nextFocusForward property and points to the descendant control in the root container, then the first control is in the front (corresponding situation: the first control is not the beginning or end of the chain).
4. In other cases, the two controls to be compared are exchanged (corresponding situation: the first control is the end of the chain).
the next comparison is the controls that are not in the same chain. It can be seen that if the control has A source control, the sequence number of the source control in focusables will be used to replace the sequence number of the control itself for comparison. It can be seen from this that after sorting, the order of controls that are not on the chain may be affected. For example, the order of the controls in focusables is A, B, C and D, where A - > C - > D. after this sort, their order becomes A, C, D and B.
the search direction is FOCUS_FORWARD or focus_ After sorting the backward, we have to see how to find out the control. First, see focus_ How to find forward:
private static View getNextFocusable(View focused, ArrayList<View> focusables, int count) { if (focused != null) { int position = focusables.lastIndexOf(focused); if (position >= 0 && position + 1 < count) { return focusables.get(position + 1); } } if (!focusables.isEmpty()) { return focusables.get(0); } return null; }
the logic is very clear. Find the control at the next position of the focused control in focusables. If the next location is not found, for example, focused does not exist in focusables or the last location in focusables, or focused is null, the control of the first location will be returned as long as focusables is not empty. Finally, if focusables is empty, null will be returned.
look at focus first_ How to find forward:
private static View getPreviousFocusable(View focused, ArrayList<View> focusables, int count) { if (focused != null) { int position = focusables.indexOf(focused); if (position > 0) { return focusables.get(position - 1); } } if (!focusables.isEmpty()) { return focusables.get(count - 1); } return null; }
this is also the control that finds the previous position of the focused control in focusables. If the next location is not found, for example, focused does not exist in focusables or the first location in focusables, or focused is null, the control of the last location will be returned as long as focusables is not empty. Finally, if focusables is empty, null will be returned.
this completes the analysis of the next control that the user does not specify.