Home | History | Annotate | Download | only in relative
      1 /*
      2  * Copyright (C) 2011 The Android Open Source Project
      3  *
      4  * Licensed under the Eclipse Public License, Version 1.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.eclipse.org/org/documents/epl-v10.php
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.android.ide.common.layout.relative;
     17 
     18 import static com.android.SdkConstants.ATTR_ID;
     19 import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX;
     20 import static com.android.SdkConstants.VALUE_TRUE;
     21 
     22 
     23 import com.android.SdkConstants;
     24 import static com.android.SdkConstants.ANDROID_URI;
     25 import com.android.ide.common.api.IDragElement;
     26 import com.android.ide.common.api.IDragElement.IDragAttribute;
     27 import com.android.ide.common.api.INode;
     28 import com.android.ide.common.api.INode.IAttribute;
     29 import com.android.ide.common.layout.BaseLayoutRule;
     30 
     31 import java.util.ArrayList;
     32 import java.util.Collection;
     33 import java.util.HashMap;
     34 import java.util.HashSet;
     35 import java.util.List;
     36 import java.util.Map;
     37 import java.util.Set;
     38 
     39 /**
     40  * Data structure about relative layout relationships which makes it possible to:
     41  * <ul>
     42  * <li> Quickly determine not just the dependencies on other nodes, but which nodes
     43  *    depend on this node such that they can be visualized for the selection
     44  * <li> Determine if there are cyclic dependencies, and whether a potential move
     45  *    would result in a cycle
     46  * <li> Determine the "depth" of a given node (in terms of how many connections it
     47  *     is away from a parent edge) such that we can prioritize connections which
     48  *     minimizes the depth
     49  * </ul>
     50  */
     51 class DependencyGraph {
     52     /** Format to chain include cycles in: a=>b=>c=>d etc */
     53     static final String CHAIN_FORMAT = "%1$s=>%2$s"; //$NON-NLS-1$
     54 
     55     /** Format to chain constraint dependencies: button 1 above button2 etc */
     56     private static final String DEPENDENCY_FORMAT = "%1$s %2$s %3$s"; //$NON-NLS-1$
     57 
     58     private final Map<String, ViewData> mIdToView = new HashMap<String, ViewData>();
     59     private final Map<INode, ViewData> mNodeToView = new HashMap<INode, ViewData>();
     60 
     61     /** Constructs a new {@link DependencyGraph} for the given relative layout */
     62     DependencyGraph(INode layout) {
     63         INode[] nodes = layout.getChildren();
     64 
     65         // Parent view:
     66         String parentId = layout.getStringAttr(ANDROID_URI, ATTR_ID);
     67         if (parentId != null) {
     68             parentId = BaseLayoutRule.stripIdPrefix(parentId);
     69         } else {
     70             parentId = "RelativeLayout"; // For display purposes; we never reference
     71             // the parent id from a constraint, only via parent-relative params
     72             // like centerInParent
     73         }
     74         ViewData parentView = new ViewData(layout, parentId);
     75         mNodeToView.put(layout, parentView);
     76         if (parentId != null) {
     77             mIdToView.put(parentId, parentView);
     78         }
     79 
     80         for (INode child : nodes) {
     81             String id = child.getStringAttr(ANDROID_URI, ATTR_ID);
     82             if (id != null) {
     83                 id = BaseLayoutRule.stripIdPrefix(id);
     84             }
     85             ViewData view = new ViewData(child, id);
     86             mNodeToView.put(child, view);
     87             if (id != null) {
     88                 mIdToView.put(id, view);
     89             }
     90         }
     91 
     92         for (ViewData view : mNodeToView.values()) {
     93             for (IAttribute attribute : view.node.getLiveAttributes()) {
     94                 String name = attribute.getName();
     95                 ConstraintType type = ConstraintType.fromAttribute(name);
     96                 if (type != null) {
     97                     String value = attribute.getValue();
     98 
     99                     if (type.targetParent) {
    100                         if (value.equals(VALUE_TRUE)) {
    101                             Constraint constraint = new Constraint(type, view, parentView);
    102                             view.dependsOn.add(constraint);
    103                             parentView.dependedOnBy.add(constraint);
    104                         }
    105                     } else {
    106                         // id-based constraint.
    107                         // NOTE: The id could refer to some widget that is NOT a sibling!
    108                         String targetId = BaseLayoutRule.stripIdPrefix(value);
    109                         ViewData target = mIdToView.get(targetId);
    110                         if (target == view) {
    111                             // Self-reference. RelativeLayout ignores these so it's
    112                             // not an error like a deeper cycle (where RelativeLayout
    113                             // will throw an exception), but we might as well warn
    114                             // the user about it.
    115                             // TODO: Where do we emit this error?
    116                         } else if (target != null) {
    117                             Constraint constraint = new Constraint(type, view, target);
    118                             view.dependsOn.add(constraint);
    119                             target.dependedOnBy.add(constraint);
    120                         } else {
    121                             // This is valid but we might want to warn...
    122                             //System.out.println("Warning: no view data found for " + targetId);
    123                         }
    124                     }
    125                 }
    126             }
    127         }
    128     }
    129 
    130     public ViewData getView(IDragElement element) {
    131         IDragAttribute attribute = element.getAttribute(ANDROID_URI, ATTR_ID);
    132         if (attribute != null) {
    133             String id = attribute.getValue();
    134             id = BaseLayoutRule.stripIdPrefix(id);
    135             return getView(id);
    136         }
    137 
    138         return null;
    139     }
    140 
    141     public ViewData getView(String id) {
    142         return mIdToView.get(id);
    143     }
    144 
    145     public ViewData getView(INode node) {
    146         return mNodeToView.get(node);
    147     }
    148 
    149     /**
    150      * Returns the set of views that depend on the given node in either the horizontal or
    151      * vertical direction
    152      *
    153      * @param nodes the set of nodes that we want to compute the transitive dependencies
    154      *            for
    155      * @param vertical if true, look for vertical edge dependencies, otherwise look for
    156      *            horizontal edge dependencies
    157      * @return the set of nodes that directly or indirectly depend on the given nodes in
    158      *         the given direction
    159      */
    160     public Set<INode> dependsOn(Collection<? extends INode> nodes, boolean vertical) {
    161         List<ViewData> reachable = new ArrayList<ViewData>();
    162 
    163         // Traverse the graph of constraints and determine all nodes affected by
    164         // this node
    165         Set<ViewData> visiting = new HashSet<ViewData>();
    166         for (INode node : nodes) {
    167             ViewData view = mNodeToView.get(node);
    168             if (view != null) {
    169                 findBackwards(view, visiting, reachable, vertical, view);
    170             }
    171         }
    172 
    173         Set<INode> dependents = new HashSet<INode>(reachable.size());
    174 
    175         for (ViewData v : reachable) {
    176             dependents.add(v.node);
    177         }
    178 
    179         return dependents;
    180     }
    181 
    182     private void findBackwards(ViewData view,
    183             Set<ViewData> visiting, List<ViewData> reachable,
    184             boolean vertical, ViewData start) {
    185         visiting.add(view);
    186         reachable.add(view);
    187 
    188         for (Constraint constraint : view.dependedOnBy) {
    189             if (vertical && !constraint.type.verticalEdge) {
    190                 continue;
    191             } else  if (!vertical && !constraint.type.horizontalEdge) {
    192                 continue;
    193             }
    194 
    195             assert constraint.to == view;
    196             ViewData from = constraint.from;
    197             if (visiting.contains(from)) {
    198                 // Cycle - what do we do to highlight this?
    199                 List<Constraint> path = getPathTo(start.node, view.node, vertical);
    200                 if (path != null) {
    201                     // TODO: display to the user somehow. We need log access for the
    202                     // view rules.
    203                     System.out.println(Constraint.describePath(path, null, null));
    204                 }
    205             } else {
    206                 findBackwards(from, visiting, reachable, vertical, start);
    207             }
    208         }
    209 
    210         visiting.remove(view);
    211     }
    212 
    213     public List<Constraint> getPathTo(INode from, INode to, boolean vertical) {
    214         // Traverse the graph of constraints and determine all nodes affected by
    215         // this node
    216         Set<ViewData> visiting = new HashSet<ViewData>();
    217         List<Constraint> path = new ArrayList<Constraint>();
    218         ViewData view = mNodeToView.get(from);
    219         if (view != null) {
    220             return findForwards(view, visiting, path, vertical, to);
    221         }
    222 
    223         return null;
    224     }
    225 
    226     private List<Constraint> findForwards(ViewData view, Set<ViewData> visiting,
    227             List<Constraint> path, boolean vertical, INode target) {
    228         visiting.add(view);
    229 
    230         for (Constraint constraint : view.dependsOn) {
    231             if (vertical && !constraint.type.verticalEdge) {
    232                 continue;
    233             } else  if (!vertical && !constraint.type.horizontalEdge) {
    234                 continue;
    235             }
    236 
    237             try {
    238                 path.add(constraint);
    239 
    240                 if (constraint.to.node == target) {
    241                     return new ArrayList<Constraint>(path);
    242                 }
    243 
    244                 assert constraint.from == view;
    245                 ViewData to = constraint.to;
    246                 if (visiting.contains(to)) {
    247                     // CYCLE!
    248                     continue;
    249                 }
    250 
    251                 List<Constraint> chain = findForwards(to, visiting, path, vertical, target);
    252                 if (chain != null) {
    253                     return chain;
    254                 }
    255             } finally {
    256                 path.remove(constraint);
    257             }
    258         }
    259 
    260         visiting.remove(view);
    261 
    262         return null;
    263     }
    264 
    265     /**
    266      * Info about a specific widget child of a relative layout and its constraints. This
    267      * is a node in the dependency graph.
    268      */
    269     static class ViewData {
    270         public final INode node;
    271         public final String id;
    272         public final List<Constraint> dependsOn = new ArrayList<Constraint>(4);
    273         public final List<Constraint> dependedOnBy = new ArrayList<Constraint>(8);
    274 
    275         ViewData(INode node, String id) {
    276             this.node = node;
    277             this.id = id;
    278         }
    279     }
    280 
    281     /**
    282      * Info about a specific constraint between two widgets in a relative layout. This is
    283      * an edge in the dependency graph.
    284      */
    285     static class Constraint {
    286         public final ConstraintType type;
    287         public final ViewData from;
    288         public final ViewData to;
    289 
    290         // TODO: Initialize depth -- should be computed independently for top, left, etc.
    291         // We can use this in GuidelineHandler.MatchComparator to prefer matches that
    292         // are closer to a parent edge:
    293         //public int depth;
    294 
    295         Constraint(ConstraintType type, ViewData from, ViewData to) {
    296             this.type = type;
    297             this.from = from;
    298             this.to = to;
    299         }
    300 
    301         static String describePath(List<Constraint> path, String newName, String newId) {
    302             String s = "";
    303             for (int i = path.size() - 1; i >= 0; i--) {
    304                 Constraint constraint = path.get(i);
    305                 String suffix = (i == path.size() -1) ? constraint.to.id : s;
    306                 s = String.format(DEPENDENCY_FORMAT, constraint.from.id,
    307                         stripLayoutAttributePrefix(constraint.type.name), suffix);
    308             }
    309 
    310             if (newName != null) {
    311                 s = String.format(DEPENDENCY_FORMAT, s, stripLayoutAttributePrefix(newName),
    312                         BaseLayoutRule.stripIdPrefix(newId));
    313             }
    314 
    315             return s;
    316         }
    317 
    318         private static String stripLayoutAttributePrefix(String name) {
    319             if (name.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)) {
    320                 return name.substring(ATTR_LAYOUT_RESOURCE_PREFIX.length());
    321             }
    322 
    323             return name;
    324         }
    325     }
    326 }
    327