Java tutorial
/* * Copyright (C) 2015 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 com.android.utils.traversal; import android.graphics.Rect; import android.support.v4.os.BuildCompat; import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; import com.android.utils.AccessibilityNodeInfoUtils; import com.android.utils.NodeFilter; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; public class DirectionalTraversalStrategy implements TraversalStrategy { /** * The root node within which to traverse. */ private AccessibilityNodeInfoCompat mRoot; /** * The cached on-screen bounds of the root node. */ private Rect mRootRect; /** * The bounds of the root node, padded slightly for intersection checks. */ private Rect mRootRectPadded; /** * A list of all nodes in mRoot's hierarchy. */ private final List<AccessibilityNodeInfoCompat> mAllNodes = new ArrayList<>(); /** * A list of only focusable nodes. */ private final List<AccessibilityNodeInfoCompat> mFocusables = new ArrayList<>(); /** * The set of focusable nodes that have focusable descendants. */ private final Set<AccessibilityNodeInfoCompat> mContainers = new HashSet<>(); /** * Cache of nodes that have speech for use by AccessibilityNodeInfoUtils. */ private final Map<AccessibilityNodeInfoCompat, Boolean> mSpeakingNodesCache = new HashMap<>(); public DirectionalTraversalStrategy(AccessibilityNodeInfoCompat root) { mRoot = AccessibilityNodeInfoCompat.obtain(root); mRootRect = new Rect(); mRoot.getBoundsInScreen(mRootRect); int fudge = -(mRootRect.width() / 20); // 5% fudge factor to catch objects near edge. mRootRectPadded = new Rect(mRootRect); mRootRectPadded.inset(fudge, fudge); processNodes(mRoot, false /* forceRefresh */); // Before N, sometimes AccessibilityNodeInfo is not properly updated after transitions // occur. This was fixed in a system framework change for N. (See BUG for context.) // To work-around, manually refresh AccessibilityNodeInfo if it initially // looks like there's nothing to focus on. if (mFocusables.size() == 0 && !BuildCompat.isAtLeastN()) { recycle(false /* recycleRoot */); processNodes(mRoot, true /* forceRefresh */); } } /** * Goes through root and its descendant nodes, sorting out the focusable nodes and the * container nodes for use in finding focus. * @return whether the root is focusable or has focusable children in its hierarchy */ private boolean processNodes(AccessibilityNodeInfoCompat root, boolean forceRefresh) { if (root == null) { return false; } if (forceRefresh) { root.refresh(); } Rect currentRect = new Rect(); root.getBoundsInScreen(currentRect); // Determine if the node is inside mRootRect (within a fudge factor). If it is outside, we // will optimize by skipping its entire hierarchy. if (!Rect.intersects(currentRect, mRootRectPadded)) { return false; } AccessibilityNodeInfoCompat rootNode = AccessibilityNodeInfoCompat.obtain(root); mAllNodes.add(rootNode); boolean isFocusable = AccessibilityNodeInfoUtils.shouldFocusNode(rootNode, mSpeakingNodesCache); if (isFocusable) { mFocusables.add(rootNode); } boolean hasFocusableDescendants = false; int childCount = rootNode.getChildCount(); for (int i = 0; i < childCount; ++i) { AccessibilityNodeInfoCompat child = rootNode.getChild(i); if (child != null) { hasFocusableDescendants |= processNodes(child, forceRefresh); child.recycle(); } } if (hasFocusableDescendants) { mContainers.add(rootNode); } return isFocusable || hasFocusableDescendants; } @Override public AccessibilityNodeInfoCompat findFocus(AccessibilityNodeInfoCompat startNode, int direction) { if (startNode == null) { return null; } else if (startNode.equals(mRoot)) { return getFirstOrderedFocus(); } Rect focusedRect = new Rect(); getAssumedRectInScreen(startNode, focusedRect); return findFocus(startNode, focusedRect, direction); } public AccessibilityNodeInfoCompat findFocus(AccessibilityNodeInfoCompat focused, Rect focusedRect, int direction) { // Using roughly the same algorithm as // frameworks/base/core/java/android/view/FocusFinder.java#findNextFocusInAbsoluteDirection Rect bestCandidateRect = new Rect(focusedRect); switch (direction) { case TraversalStrategy.SEARCH_FOCUS_LEFT: bestCandidateRect.offset(focusedRect.width() + 1, 0); break; case TraversalStrategy.SEARCH_FOCUS_RIGHT: bestCandidateRect.offset(-(focusedRect.width() + 1), 0); break; case TraversalStrategy.SEARCH_FOCUS_UP: bestCandidateRect.offset(0, focusedRect.height() + 1); break; case TraversalStrategy.SEARCH_FOCUS_DOWN: bestCandidateRect.offset(0, -(focusedRect.height() + 1)); break; } AccessibilityNodeInfoCompat closest = null; for (AccessibilityNodeInfoCompat focusable : mFocusables) { // Skip the currently-focused view. if (focusable.equals(focused) || focusable.equals(mRoot)) { continue; } Rect otherRect = new Rect(); getAssumedRectInScreen(focusable, otherRect); if (isBetterCandidate(direction, focusedRect, otherRect, bestCandidateRect)) { bestCandidateRect.set(otherRect); closest = focusable; } } if (closest != null) { return AccessibilityNodeInfoCompat.obtain(closest); } return null; } /** * Selects an item to focus when there is no current accessibility focus. * * Uses a two-pronged strategy. First tries to see if there is an input-focused node, and if so, * returns that node. * Otherwise, returns the item that an OrderedTraversalStrategy would first focus; this has the * advantage of working nicely for both LTR and RTL users. */ private AccessibilityNodeInfoCompat getFirstOrderedFocus() { NodeFilter filter = new NodeFilter() { @Override public boolean accept(AccessibilityNodeInfoCompat node) { return node != null && mFocusables.contains(node); } }; // 1. Attempt to find input-focused node. AccessibilityNodeInfoCompat inputFocused = mRoot.findFocus(AccessibilityNodeInfoCompat.FOCUS_INPUT); try { AccessibilityNodeInfoCompat target = AccessibilityNodeInfoUtils.getSelfOrMatchingAncestor(inputFocused, filter); if (target != null) { return target; } } finally { if (inputFocused != null) { inputFocused.recycle(); } } // 2. Just use the OrderedTraversalStrategy. final OrderedTraversalStrategy orderedStrategy = new OrderedTraversalStrategy(mRoot); try { // Should not need to obtain() here; the inner code should do this for us. return AccessibilityNodeInfoUtils.searchFocus(orderedStrategy, mRoot, TraversalStrategy.SEARCH_FOCUS_FORWARD, filter); } finally { orderedStrategy.recycle(); } } @Override public AccessibilityNodeInfoCompat focusInitial(AccessibilityNodeInfoCompat root, int direction) { if (root == null) { return null; } Rect rootRect = new Rect(); root.getBoundsInScreen(rootRect); AccessibilityNodeInfoCompat focusedNode = root.findFocus(AccessibilityNodeInfoCompat.FOCUS_ACCESSIBILITY); Rect searchRect = new Rect(); if (focusedNode != null) { getSearchStartRect(focusedNode, direction, searchRect); } else if (direction == TraversalStrategy.SEARCH_FOCUS_LEFT) { searchRect.set(rootRect.right, rootRect.top, rootRect.right + 1, rootRect.bottom); } else if (direction == TraversalStrategy.SEARCH_FOCUS_RIGHT) { searchRect.set(rootRect.left - 1, rootRect.top, rootRect.left, rootRect.bottom); } else if (direction == TraversalStrategy.SEARCH_FOCUS_UP) { searchRect.set(rootRect.left, rootRect.bottom, rootRect.right, rootRect.bottom + 1); } else { searchRect.set(rootRect.left, rootRect.top - 1, rootRect.right, rootRect.top); } AccessibilityNodeInfoCompat newFocus = findFocus(focusedNode, searchRect, direction); if (newFocus != null) { return AccessibilityNodeInfoCompat.obtain(newFocus); } return null; } @Override public Map<AccessibilityNodeInfoCompat, Boolean> getSpeakingNodesCache() { return null; } private void recycle(boolean recycleRoot) { for (AccessibilityNodeInfoCompat node : mAllNodes) { node.recycle(); } mAllNodes.clear(); mFocusables.clear(); // No recycle needed for mFocusables or mContainers because their mContainers.clear(); // nodes were already recycled from mAllNodes. mSpeakingNodesCache.clear(); if (recycleRoot) { mRoot.recycle(); mRoot = null; } } @Override public void recycle() { recycle(true); } /** * Returns the bounding rect of the given node for directional navigation purposes. * Any node that is a container of a focusable node will be reduced to a strip at its very * top edge. */ private void getAssumedRectInScreen(AccessibilityNodeInfoCompat node, Rect assumedRect) { node.getBoundsInScreen(assumedRect); if (mContainers.contains(node)) { assumedRect.set(assumedRect.left, assumedRect.top, assumedRect.right, assumedRect.top + 1); } } /** * Given a focus rectangle, returns another rectangle that is placed at the beginning of the * row or column of the focused object, depending on the direction in which we are navigating. * * Example: * <pre> * +---------+ * | | node=# * A| # | When direction=TraversalStrategy.SEARCH_FOCUS_RIGHT, then a rectangle A with * | | same width and height as node gets returned. * | | When direction=TraversalStrategy.SEARCH_FOCUS_UP, then a rectangle B with same * +---------+ width and height as node gets returned. * B * </pre> */ private void getSearchStartRect(AccessibilityNodeInfoCompat node, int direction, Rect rect) { Rect focusedRect = new Rect(); node.getBoundsInScreen(focusedRect); Rect rootBounds = new Rect(); mRoot.getBoundsInScreen(rootBounds); switch (direction) { case TraversalStrategy.SEARCH_FOCUS_LEFT: // Start from right and move leftwards. rect.set(rootBounds.right, focusedRect.top, rootBounds.right + focusedRect.width(), focusedRect.bottom); break; case TraversalStrategy.SEARCH_FOCUS_RIGHT: // Start from left and move rightwards. rect.set(rootBounds.left - focusedRect.width(), focusedRect.top, rootBounds.left, focusedRect.bottom); break; case TraversalStrategy.SEARCH_FOCUS_UP: // Start from bottom and move upwards. rect.set(focusedRect.left, rootBounds.bottom, focusedRect.right, rootBounds.bottom + focusedRect.height()); break; case TraversalStrategy.SEARCH_FOCUS_DOWN: // Start from top and move downwards. rect.set(focusedRect.left, rootBounds.top - focusedRect.height(), focusedRect.right, rootBounds.top); break; default: throw new IllegalArgumentException("direction must be a SearchDirection"); } } /* * BEGIN CODE COPIED FROM frameworks/base/core/java/android/view/FocusFinder.java * These lines were last revised 2009-03-03 in revision 9066cfe9. * Modifications from original: * - Uses TraversalStrategy.SEARCH_FOCUS_* constants instead of View.FOCUS_* constants * - getWeightedDistanceFor() returns MAX_VALUE for very large values to prevent overflow */ /** * Is rect1 a better candidate than rect2 for a focus search in a particular * direction from a source rect? This is the core routine that determines * the order of focus searching. * @param direction the direction (up, down, left, right) * @param source The source we are searching from * @param rect1 The candidate rectangle * @param rect2 The current best candidate. * @return Whether the candidate is the new best. */ 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))); } /** * One rectangle may be another candidate than another by virtue of being * exclusively in the beam of the source rect. * @return Whether rect1 is a better candidate than rect2 by virtue of it being in src's * beam */ 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 == TraversalStrategy.SEARCH_FOCUS_LEFT || direction == TraversalStrategy.SEARCH_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)); } /** * 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. */ int getWeightedDistanceFor(int majorAxisDistance, int minorAxisDistance) { if (majorAxisDistance > 10000 || minorAxisDistance > 10000) { return Integer.MAX_VALUE; } else { // Won't overflow; max possible value = 1400000000 < Integer.MAX_VALUE. return 13 * majorAxisDistance * majorAxisDistance + minorAxisDistance * minorAxisDistance; } } /** * Is destRect a candidate for the next focus given the direction? This * checks whether the dest is at least partially to the direction of (e.g left of) * from source. * * Includes an edge case for an empty rect (which is used in some cases when * searching from a point on the screen). */ boolean isCandidate(Rect srcRect, Rect destRect, int direction) { switch (direction) { case TraversalStrategy.SEARCH_FOCUS_LEFT: return (srcRect.right > destRect.right || srcRect.left >= destRect.right) && srcRect.left > destRect.left; case TraversalStrategy.SEARCH_FOCUS_RIGHT: return (srcRect.left < destRect.left || srcRect.right <= destRect.left) && srcRect.right < destRect.right; case TraversalStrategy.SEARCH_FOCUS_UP: return (srcRect.bottom > destRect.bottom || srcRect.top >= destRect.bottom) && srcRect.top > destRect.top; case TraversalStrategy.SEARCH_FOCUS_DOWN: return (srcRect.top < destRect.top || srcRect.bottom <= destRect.top) && srcRect.bottom < destRect.bottom; } throw new IllegalArgumentException("direction must be a SearchDirection"); } /** * Do the "beams" w.r.t the given direction's axis of rect1 and rect2 overlap? * @param direction the direction (up, down, left, right) * @param rect1 The first rectangle * @param rect2 The second rectangle * @return whether the beams overlap */ boolean beamsOverlap(int direction, Rect rect1, Rect rect2) { switch (direction) { case TraversalStrategy.SEARCH_FOCUS_LEFT: case TraversalStrategy.SEARCH_FOCUS_RIGHT: return (rect2.bottom >= rect1.top) && (rect2.top <= rect1.bottom); case TraversalStrategy.SEARCH_FOCUS_UP: case TraversalStrategy.SEARCH_FOCUS_DOWN: return (rect2.right >= rect1.left) && (rect2.left <= rect1.right); } throw new IllegalArgumentException("direction must be a SearchDirection"); } /** * e.g for left, is 'to left of' */ boolean isToDirectionOf(int direction, Rect src, Rect dest) { switch (direction) { case TraversalStrategy.SEARCH_FOCUS_LEFT: return src.left >= dest.right; case TraversalStrategy.SEARCH_FOCUS_RIGHT: return src.right <= dest.left; case TraversalStrategy.SEARCH_FOCUS_UP: return src.top >= dest.bottom; case TraversalStrategy.SEARCH_FOCUS_DOWN: return src.bottom <= dest.top; } throw new IllegalArgumentException("direction must be a SearchDirection"); } /** * @return The distance from the edge furthest in the given direction * of source to the edge nearest in the given direction of dest. If the * dest is not in the direction from source, return 0. */ 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 TraversalStrategy.SEARCH_FOCUS_LEFT: return source.left - dest.right; case TraversalStrategy.SEARCH_FOCUS_RIGHT: return dest.left - source.right; case TraversalStrategy.SEARCH_FOCUS_UP: return source.top - dest.bottom; case TraversalStrategy.SEARCH_FOCUS_DOWN: return dest.top - source.bottom; } throw new IllegalArgumentException("direction must be a SearchDirection"); } /** * @return The distance along the major axis w.r.t the direction from the * edge of source to the far edge of dest. If the * dest is not in the direction from source, return 1 (to break ties with * {@link #majorAxisDistance}). */ 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 TraversalStrategy.SEARCH_FOCUS_LEFT: return source.left - dest.left; case TraversalStrategy.SEARCH_FOCUS_RIGHT: return dest.right - source.right; case TraversalStrategy.SEARCH_FOCUS_UP: return source.top - dest.top; case TraversalStrategy.SEARCH_FOCUS_DOWN: return dest.bottom - source.bottom; } throw new IllegalArgumentException("direction must be a SearchDirection"); } /** * Find the distance on the minor axis w.r.t the direction to the nearest * edge of the destination rectangle. * @param direction the direction (up, down, left, right) * @param source The source rect. * @param dest The destination rect. * @return The distance. */ static int minorAxisDistance(int direction, Rect source, Rect dest) { switch (direction) { case TraversalStrategy.SEARCH_FOCUS_LEFT: case TraversalStrategy.SEARCH_FOCUS_RIGHT: // the distance between the center verticals return Math.abs(((source.top + source.height() / 2) - ((dest.top + dest.height() / 2)))); case TraversalStrategy.SEARCH_FOCUS_UP: case TraversalStrategy.SEARCH_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 a SearchDirection"); } /* END CODE COPIED FROM frameworks/base/core/java/android/view/FocusFinder.java */ }