/*
* Copyright (C) 2015 Google Inc.
*
* 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.switchaccess.treebuilding;
import android.content.Context;
import android.graphics.Rect;
import com.android.switchaccess.ClearFocusNode;
import com.android.switchaccess.OptionScanNode;
import com.android.switchaccess.OptionScanSelectionNode;
import com.android.switchaccess.SwitchAccessNodeCompat;
import com.android.switchaccess.SwitchAccessWindowInfo;
import java.util.Comparator;
import java.util.List;
import java.util.SortedMap;
import java.util.TreeMap;
/**
* Build an option scanning tree for row-column scanning. The rows are linear scanned, as are the
* elements withing the row.
* Note that this builder ignores the hierarchy of the views entirely. It just groups Views based
* on their spatial location. That works fine for something like a keyboard, but will not be
* ideal for all UIs.
*/
public class RowColumnTreeBuilder extends BinaryTreeBuilder {
/* Any rows shorter than this should just be linearly scanned */
private static int MIN_NODES_PER_ROW = 3;
private static final Comparator<RowBounds> ROW_BOUNDS_COMPARATOR = new Comparator<RowBounds>() {
@Override
public int compare(RowBounds rowBounds, RowBounds t1) {
if (rowBounds.mTop != t1.mTop) {
/* Want higher y coords to be traversed later */
return t1.mTop - rowBounds.mTop;
}
/* Want larger views to be traversed earlier */
return rowBounds.mBottom - t1.mBottom;
}
};
private static class RowBounds {
private final int mTop, mBottom;
public RowBounds(int top, int bottom) {
mTop = top;
mBottom = bottom;
}
@Override
public int hashCode() {
/* Not the most general hash, but sufficient for reasonable screen sizes */
return (mTop << 16) + mBottom;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof RowBounds)) {
return false;
}
return (((RowBounds) o).mTop == mTop) && (((RowBounds) o).mBottom == mBottom);
}
}
public RowColumnTreeBuilder(Context context) {
super(context);
}
@Override
public OptionScanNode addViewHierarchyToTree(SwitchAccessNodeCompat root,
OptionScanNode treeToBuildOn) {
OptionScanNode tree = (treeToBuildOn != null) ? treeToBuildOn : new ClearFocusNode();
SortedMap<RowBounds, SortedMap<Integer, SwitchAccessNodeCompat>> nodesByXYCoordinate =
getMapOfNodesByXYCoordinate(root);
for (SortedMap<Integer, SwitchAccessNodeCompat> nodesInThisRow
: nodesByXYCoordinate.values()) {
if (nodesInThisRow.size() < MIN_NODES_PER_ROW) {
for (SwitchAccessNodeCompat node : nodesInThisRow.values()) {
tree = addCompatToTree(node, tree);
node.recycle();
}
} else {
OptionScanNode rowTree = new ClearFocusNode();
for (SwitchAccessNodeCompat node : nodesInThisRow.values()) {
rowTree = addCompatToTree(node, rowTree);
node.recycle();
}
tree = new OptionScanSelectionNode(rowTree, tree);
}
}
return tree;
}
@Override
public OptionScanNode addWindowListToTree(List<SwitchAccessWindowInfo> windowList,
OptionScanNode treeToBuildOn) {
/* Not currently needed */
return null;
}
private SortedMap<RowBounds, SortedMap<Integer, SwitchAccessNodeCompat>>
getMapOfNodesByXYCoordinate(SwitchAccessNodeCompat root) {
SortedMap<RowBounds, SortedMap<Integer, SwitchAccessNodeCompat>> nodesByXYCoordinate =
new TreeMap<>(ROW_BOUNDS_COMPARATOR);
List<SwitchAccessNodeCompat> talkBackOrderList = getNodesInTalkBackOrder(root);
Rect boundsInScreen = new Rect();
for (SwitchAccessNodeCompat node : talkBackOrderList) {
/* Only add the node to list if it will be added to the tree */
OptionScanNode treeWithCurrentNode = addCompatToTree(node, new ClearFocusNode());
if (treeWithCurrentNode instanceof OptionScanSelectionNode) {
node.getVisibleBoundsInScreen(boundsInScreen);
/*
* Use negative value so traversal will start with the last elements, so the first
* ones end up at the top of the tree.
*/
RowBounds rowBounds = new RowBounds(boundsInScreen.top, boundsInScreen.bottom);
SortedMap<Integer, SwitchAccessNodeCompat> mapOfNodes =
nodesByXYCoordinate.get(rowBounds);
if (mapOfNodes == null) {
mapOfNodes = new TreeMap<>();
nodesByXYCoordinate.put(rowBounds, mapOfNodes);
}
mapOfNodes.put(-boundsInScreen.left, node);
} else {
node.recycle();
}
treeWithCurrentNode.recycle();
}
return nodesByXYCoordinate;
}
}