/* * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php * * 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.ide.common.layout.relative; import static com.android.SdkConstants.ATTR_ID; import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX; import static com.android.SdkConstants.VALUE_TRUE; import com.android.SdkConstants; import static com.android.SdkConstants.ANDROID_URI; import com.android.ide.common.api.IDragElement; import com.android.ide.common.api.IDragElement.IDragAttribute; import com.android.ide.common.api.INode; import com.android.ide.common.api.INode.IAttribute; import com.android.ide.common.layout.BaseLayoutRule; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; /** * Data structure about relative layout relationships which makes it possible to: * <ul> * <li> Quickly determine not just the dependencies on other nodes, but which nodes * depend on this node such that they can be visualized for the selection * <li> Determine if there are cyclic dependencies, and whether a potential move * would result in a cycle * <li> Determine the "depth" of a given node (in terms of how many connections it * is away from a parent edge) such that we can prioritize connections which * minimizes the depth * </ul> */ class DependencyGraph { /** Format to chain include cycles in: a=>b=>c=>d etc */ static final String CHAIN_FORMAT = "%1$s=>%2$s"; //$NON-NLS-1$ /** Format to chain constraint dependencies: button 1 above button2 etc */ private static final String DEPENDENCY_FORMAT = "%1$s %2$s %3$s"; //$NON-NLS-1$ private final Map<String, ViewData> mIdToView = new HashMap<String, ViewData>(); private final Map<INode, ViewData> mNodeToView = new HashMap<INode, ViewData>(); /** Constructs a new {@link DependencyGraph} for the given relative layout */ DependencyGraph(INode layout) { INode[] nodes = layout.getChildren(); // Parent view: String parentId = layout.getStringAttr(ANDROID_URI, ATTR_ID); if (parentId != null) { parentId = BaseLayoutRule.stripIdPrefix(parentId); } else { parentId = "RelativeLayout"; // For display purposes; we never reference // the parent id from a constraint, only via parent-relative params // like centerInParent } ViewData parentView = new ViewData(layout, parentId); mNodeToView.put(layout, parentView); if (parentId != null) { mIdToView.put(parentId, parentView); } for (INode child : nodes) { String id = child.getStringAttr(ANDROID_URI, ATTR_ID); if (id != null) { id = BaseLayoutRule.stripIdPrefix(id); } ViewData view = new ViewData(child, id); mNodeToView.put(child, view); if (id != null) { mIdToView.put(id, view); } } for (ViewData view : mNodeToView.values()) { for (IAttribute attribute : view.node.getLiveAttributes()) { String name = attribute.getName(); ConstraintType type = ConstraintType.fromAttribute(name); if (type != null) { String value = attribute.getValue(); if (type.targetParent) { if (value.equals(VALUE_TRUE)) { Constraint constraint = new Constraint(type, view, parentView); view.dependsOn.add(constraint); parentView.dependedOnBy.add(constraint); } } else { // id-based constraint. // NOTE: The id could refer to some widget that is NOT a sibling! String targetId = BaseLayoutRule.stripIdPrefix(value); ViewData target = mIdToView.get(targetId); if (target == view) { // Self-reference. RelativeLayout ignores these so it's // not an error like a deeper cycle (where RelativeLayout // will throw an exception), but we might as well warn // the user about it. // TODO: Where do we emit this error? } else if (target != null) { Constraint constraint = new Constraint(type, view, target); view.dependsOn.add(constraint); target.dependedOnBy.add(constraint); } else { // This is valid but we might want to warn... //System.out.println("Warning: no view data found for " + targetId); } } } } } } public ViewData getView(IDragElement element) { IDragAttribute attribute = element.getAttribute(ANDROID_URI, ATTR_ID); if (attribute != null) { String id = attribute.getValue(); id = BaseLayoutRule.stripIdPrefix(id); return getView(id); } return null; } public ViewData getView(String id) { return mIdToView.get(id); } public ViewData getView(INode node) { return mNodeToView.get(node); } /** * Returns the set of views that depend on the given node in either the horizontal or * vertical direction * * @param nodes the set of nodes that we want to compute the transitive dependencies * for * @param vertical if true, look for vertical edge dependencies, otherwise look for * horizontal edge dependencies * @return the set of nodes that directly or indirectly depend on the given nodes in * the given direction */ public Set<INode> dependsOn(Collection<? extends INode> nodes, boolean vertical) { List<ViewData> reachable = new ArrayList<ViewData>(); // Traverse the graph of constraints and determine all nodes affected by // this node Set<ViewData> visiting = new HashSet<ViewData>(); for (INode node : nodes) { ViewData view = mNodeToView.get(node); if (view != null) { findBackwards(view, visiting, reachable, vertical, view); } } Set<INode> dependents = new HashSet<INode>(reachable.size()); for (ViewData v : reachable) { dependents.add(v.node); } return dependents; } private void findBackwards(ViewData view, Set<ViewData> visiting, List<ViewData> reachable, boolean vertical, ViewData start) { visiting.add(view); reachable.add(view); for (Constraint constraint : view.dependedOnBy) { if (vertical && !constraint.type.verticalEdge) { continue; } else if (!vertical && !constraint.type.horizontalEdge) { continue; } assert constraint.to == view; ViewData from = constraint.from; if (visiting.contains(from)) { // Cycle - what do we do to highlight this? List<Constraint> path = getPathTo(start.node, view.node, vertical); if (path != null) { // TODO: display to the user somehow. We need log access for the // view rules. System.out.println(Constraint.describePath(path, null, null)); } } else { findBackwards(from, visiting, reachable, vertical, start); } } visiting.remove(view); } public List<Constraint> getPathTo(INode from, INode to, boolean vertical) { // Traverse the graph of constraints and determine all nodes affected by // this node Set<ViewData> visiting = new HashSet<ViewData>(); List<Constraint> path = new ArrayList<Constraint>(); ViewData view = mNodeToView.get(from); if (view != null) { return findForwards(view, visiting, path, vertical, to); } return null; } private List<Constraint> findForwards(ViewData view, Set<ViewData> visiting, List<Constraint> path, boolean vertical, INode target) { visiting.add(view); for (Constraint constraint : view.dependsOn) { if (vertical && !constraint.type.verticalEdge) { continue; } else if (!vertical && !constraint.type.horizontalEdge) { continue; } try { path.add(constraint); if (constraint.to.node == target) { return new ArrayList<Constraint>(path); } assert constraint.from == view; ViewData to = constraint.to; if (visiting.contains(to)) { // CYCLE! continue; } List<Constraint> chain = findForwards(to, visiting, path, vertical, target); if (chain != null) { return chain; } } finally { path.remove(constraint); } } visiting.remove(view); return null; } /** * Info about a specific widget child of a relative layout and its constraints. This * is a node in the dependency graph. */ static class ViewData { public final INode node; public final String id; public final List<Constraint> dependsOn = new ArrayList<Constraint>(4); public final List<Constraint> dependedOnBy = new ArrayList<Constraint>(8); ViewData(INode node, String id) { this.node = node; this.id = id; } } /** * Info about a specific constraint between two widgets in a relative layout. This is * an edge in the dependency graph. */ static class Constraint { public final ConstraintType type; public final ViewData from; public final ViewData to; // TODO: Initialize depth -- should be computed independently for top, left, etc. // We can use this in GuidelineHandler.MatchComparator to prefer matches that // are closer to a parent edge: //public int depth; Constraint(ConstraintType type, ViewData from, ViewData to) { this.type = type; this.from = from; this.to = to; } static String describePath(List<Constraint> path, String newName, String newId) { String s = ""; for (int i = path.size() - 1; i >= 0; i--) { Constraint constraint = path.get(i); String suffix = (i == path.size() -1) ? constraint.to.id : s; s = String.format(DEPENDENCY_FORMAT, constraint.from.id, stripLayoutAttributePrefix(constraint.type.name), suffix); } if (newName != null) { s = String.format(DEPENDENCY_FORMAT, s, stripLayoutAttributePrefix(newName), BaseLayoutRule.stripIdPrefix(newId)); } return s; } private static String stripLayoutAttributePrefix(String name) { if (name.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)) { return name.substring(ATTR_LAYOUT_RESOURCE_PREFIX.length()); } return name; } } }