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.ide.common.api.MarginType.NO_MARGIN;
     19 import static com.android.ide.common.api.MarginType.WITHOUT_MARGIN;
     20 import static com.android.ide.common.api.MarginType.WITH_MARGIN;
     21 import static com.android.ide.common.api.SegmentType.BASELINE;
     22 import static com.android.ide.common.api.SegmentType.BOTTOM;
     23 import static com.android.ide.common.api.SegmentType.CENTER_HORIZONTAL;
     24 import static com.android.ide.common.api.SegmentType.CENTER_VERTICAL;
     25 import static com.android.ide.common.api.SegmentType.LEFT;
     26 import static com.android.ide.common.api.SegmentType.RIGHT;
     27 import static com.android.ide.common.api.SegmentType.TOP;
     28 import static com.android.ide.common.layout.BaseLayoutRule.getMaxMatchDistance;
     29 import static com.android.ide.common.layout.LayoutConstants.ANDROID_URI;
     30 import static com.android.ide.common.layout.LayoutConstants.ATTR_ID;
     31 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ABOVE;
     32 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_BASELINE;
     33 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_BOTTOM;
     34 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_LEFT;
     35 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_BOTTOM;
     36 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_LEFT;
     37 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_RIGHT;
     38 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_TOP;
     39 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_RIGHT;
     40 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_TOP;
     41 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_BELOW;
     42 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_CENTER_HORIZONTAL;
     43 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_CENTER_IN_PARENT;
     44 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_CENTER_VERTICAL;
     45 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_MARGIN_BOTTOM;
     46 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_MARGIN_LEFT;
     47 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_MARGIN_RIGHT;
     48 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_MARGIN_TOP;
     49 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_TO_LEFT_OF;
     50 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_TO_RIGHT_OF;
     51 import static com.android.ide.common.layout.LayoutConstants.VALUE_N_DP;
     52 import static com.android.ide.common.layout.LayoutConstants.VALUE_TRUE;
     53 import static com.android.ide.common.layout.relative.ConstraintType.ALIGN_BASELINE;
     54 import static java.lang.Math.abs;
     55 
     56 import com.android.ide.common.api.DropFeedback;
     57 import com.android.ide.common.api.IClientRulesEngine;
     58 import com.android.ide.common.api.INode;
     59 import com.android.ide.common.api.Margins;
     60 import com.android.ide.common.api.Rect;
     61 import com.android.ide.common.api.Segment;
     62 import com.android.ide.common.api.SegmentType;
     63 import com.android.ide.common.layout.BaseLayoutRule;
     64 import com.android.ide.common.layout.relative.DependencyGraph.Constraint;
     65 import com.android.ide.common.layout.relative.DependencyGraph.ViewData;
     66 
     67 import java.util.ArrayList;
     68 import java.util.Collection;
     69 import java.util.Collections;
     70 import java.util.Comparator;
     71 import java.util.List;
     72 import java.util.Set;
     73 
     74 /**
     75  * The {@link GuidelineHandler} class keeps track of state related to a guideline operation
     76  * like move and resize, and performs various constraint computations.
     77  */
     78 public class GuidelineHandler {
     79     /**
     80      * A dependency graph for the relative layout recording constraint relationships
     81      */
     82     protected DependencyGraph mDependencyGraph;
     83 
     84     /** The RelativeLayout we are moving/resizing within */
     85     public INode layout;
     86 
     87     /** The set of nodes being dragged (may be null) */
     88     protected Collection<INode> mDraggedNodes;
     89 
     90     /** The bounds of the primary child node being dragged */
     91     protected Rect mBounds;
     92 
     93     /** Whether the left edge is being moved/resized */
     94     protected boolean mMoveLeft;
     95 
     96     /** Whether the right edge is being moved/resized */
     97     protected boolean mMoveRight;
     98 
     99     /** Whether the top edge is being moved/resized */
    100     protected boolean mMoveTop;
    101 
    102     /** Whether the bottom edge is being moved/resized */
    103     protected boolean mMoveBottom;
    104 
    105     /**
    106      * Whether the drop/move/resize position should be snapped (which can be turned off
    107      * with a modifier key during the operation)
    108      */
    109     protected boolean mSnap = true;
    110 
    111     /**
    112      * The set of nodes which depend on the currently selected nodes, including
    113      * transitively, through horizontal constraints (a "horizontal constraint"
    114      * is a constraint between two horizontal edges)
    115      */
    116     protected Set<INode> mHorizontalDeps;
    117 
    118     /**
    119      * The set of nodes which depend on the currently selected nodes, including
    120      * transitively, through vertical constraints (a "vertical constraint"
    121      * is a constraint between two vertical edges)
    122      */
    123     protected Set<INode> mVerticalDeps;
    124 
    125     /** The current list of constraints which result in a horizontal cycle (if applicable) */
    126     protected List<Constraint> mHorizontalCycle;
    127 
    128     /** The current list of constraints which result in a vertical cycle (if applicable) */
    129     protected List<Constraint> mVerticalCycle;
    130 
    131     /**
    132      * All horizontal segments in the relative layout - top and bottom edges, baseline
    133      * edges, and top and bottom edges offset by the applicable margins in each direction
    134      */
    135     protected List<Segment> mHorizontalEdges;
    136 
    137     /**
    138      * All vertical segments in the relative layout - left and right edges, and left and
    139      * right edges offset by the applicable margins in each direction
    140      */
    141     protected List<Segment> mVerticalEdges;
    142 
    143     /**
    144      * All center vertical segments in the relative layout. These are kept separate since
    145      * they only match other center edges.
    146      */
    147     protected List<Segment> mCenterVertEdges;
    148 
    149     /**
    150      * All center horizontal segments in the relative layout. These are kept separate
    151      * since they only match other center edges.
    152      */
    153     protected List<Segment> mCenterHorizEdges;
    154 
    155     /**
    156      * Suggestions for horizontal matches. There could be more than one, but all matches
    157      * will be equidistant from the current position (as well as in the same direction,
    158      * which means that you can't have one match 5 pixels to the left and one match 5
    159      * pixels to the right since it would be impossible to snap to fit with both; you can
    160      * however have multiple matches all 5 pixels to the left.)
    161      * <p
    162      * The best vertical match will be found in {@link #mCurrentTopMatch} or
    163      * {@link #mCurrentBottomMatch}.
    164      */
    165     protected List<Match> mHorizontalSuggestions;
    166 
    167     /**
    168      * Suggestions for vertical matches.
    169      * <p
    170      * The best vertical match will be found in {@link #mCurrentLeftMatch} or
    171      * {@link #mCurrentRightMatch}.
    172      */
    173     protected List<Match> mVerticalSuggestions;
    174 
    175     /**
    176      * The current match on the left edge, or null if no match or if the left edge is not
    177      * being moved or resized.
    178      */
    179     protected Match mCurrentLeftMatch;
    180 
    181     /**
    182      * The current match on the top edge, or null if no match or if the top edge is not
    183      * being moved or resized.
    184      */
    185     protected Match mCurrentTopMatch;
    186 
    187     /**
    188      * The current match on the right edge, or null if no match or if the right edge is
    189      * not being moved or resized.
    190      */
    191     protected Match mCurrentRightMatch;
    192 
    193     /**
    194      * The current match on the bottom edge, or null if no match or if the bottom edge is
    195      * not being moved or resized.
    196      */
    197     protected Match mCurrentBottomMatch;
    198 
    199     /**
    200      * The amount of margin to add to the top edge, or 0
    201      */
    202     protected int mTopMargin;
    203 
    204     /**
    205      * The amount of margin to add to the bottom edge, or 0
    206      */
    207     protected int mBottomMargin;
    208 
    209     /**
    210      * The amount of margin to add to the left edge, or 0
    211      */
    212     protected int mLeftMargin;
    213 
    214     /**
    215      * The amount of margin to add to the right edge, or 0
    216      */
    217     protected int mRightMargin;
    218 
    219     /**
    220      * The associated rules engine
    221      */
    222     protected IClientRulesEngine mRulesEngine;
    223 
    224     /**
    225      * Construct a new {@link GuidelineHandler} for the given relative layout.
    226      *
    227      * @param layout the RelativeLayout to handle
    228      */
    229     GuidelineHandler(INode layout, IClientRulesEngine rulesEngine) {
    230         this.layout = layout;
    231         mRulesEngine = rulesEngine;
    232 
    233         mHorizontalEdges = new ArrayList<Segment>();
    234         mVerticalEdges = new ArrayList<Segment>();
    235         mCenterVertEdges = new ArrayList<Segment>();
    236         mCenterHorizEdges = new ArrayList<Segment>();
    237         mDependencyGraph = new DependencyGraph(layout);
    238     }
    239 
    240     /**
    241      * Returns true if the handler has any suggestions to offer
    242      *
    243      * @return true if the handler has any suggestions to offer
    244      */
    245     public boolean haveSuggestions() {
    246         return mCurrentLeftMatch != null || mCurrentTopMatch != null
    247                 || mCurrentRightMatch != null || mCurrentBottomMatch != null;
    248     }
    249 
    250     /**
    251      * Returns the closest match.
    252      *
    253      * @return the closest match, or null if nothing matched
    254      */
    255     protected Match pickBestMatch(List<Match> matches) {
    256         int alternatives = matches.size();
    257         if (alternatives == 0) {
    258             return null;
    259         } else if (alternatives == 1) {
    260             Match match = matches.get(0);
    261             return match;
    262         } else {
    263             assert alternatives > 1;
    264             Collections.sort(matches, new MatchComparator());
    265             return matches.get(0);
    266         }
    267     }
    268 
    269     private boolean checkCycle(DropFeedback feedback, Match match, boolean vertical) {
    270         if (match != null && match.cycle) {
    271             for (INode node : mDraggedNodes) {
    272                 INode from = match.edge.node;
    273                 assert match.with.node == null || match.with.node == node;
    274                 INode to = node;
    275                 List<Constraint> path = mDependencyGraph.getPathTo(from, to, vertical);
    276                 if (path != null) {
    277                     if (vertical) {
    278                         mVerticalCycle = path;
    279                     } else {
    280                         mHorizontalCycle = path;
    281                     }
    282                     String desc = Constraint.describePath(path,
    283                             match.type.name, match.edge.id);
    284 
    285                     feedback.errorMessage = "Constraint creates a cycle: " + desc;
    286                     return true;
    287                 }
    288             }
    289         }
    290 
    291         return false;
    292     }
    293 
    294     public void checkCycles(DropFeedback feedback) {
    295         // Deliberate short circuit evaluation -- only list the first cycle
    296         feedback.errorMessage = null;
    297         mHorizontalCycle = null;
    298         mVerticalCycle = null;
    299 
    300         if (checkCycle(feedback, mCurrentTopMatch, true /* vertical */)
    301                 || checkCycle(feedback, mCurrentBottomMatch, true)) {
    302         }
    303 
    304         if (checkCycle(feedback, mCurrentLeftMatch, false)
    305                 || checkCycle(feedback, mCurrentRightMatch, false)) {
    306         }
    307     }
    308 
    309     /** Records the matchable outside edges for the given node to the potential match list */
    310     protected void addBounds(INode node, String id,
    311             boolean addHorizontal, boolean addVertical) {
    312         Rect b = node.getBounds();
    313         Margins margins = node.getMargins();
    314         if (addHorizontal) {
    315             if (margins.top != 0) {
    316                 mHorizontalEdges.add(new Segment(b.y, b.x, b.x2(), node, id, TOP, WITHOUT_MARGIN));
    317                 mHorizontalEdges.add(new Segment(b.y - margins.top, b.x, b.x2(), node, id,
    318                         TOP, WITH_MARGIN));
    319             } else {
    320                 mHorizontalEdges.add(new Segment(b.y, b.x, b.x2(), node, id, TOP, NO_MARGIN));
    321             }
    322             if (margins.bottom != 0) {
    323                 mHorizontalEdges.add(new Segment(b.y2(), b.x, b.x2(), node, id, BOTTOM,
    324                         WITHOUT_MARGIN));
    325                 mHorizontalEdges.add(new Segment(b.y2() + margins.bottom, b.x, b.x2(), node,
    326                         id, BOTTOM, WITH_MARGIN));
    327             } else {
    328                 mHorizontalEdges.add(new Segment(b.y2(), b.x, b.x2(), node, id,
    329                         BOTTOM, NO_MARGIN));
    330             }
    331         }
    332         if (addVertical) {
    333             if (margins.left != 0) {
    334                 mVerticalEdges.add(new Segment(b.x, b.y, b.y2(), node, id, LEFT, WITHOUT_MARGIN));
    335                 mVerticalEdges.add(new Segment(b.x - margins.left, b.y, b.y2(), node, id, LEFT,
    336                         WITH_MARGIN));
    337             } else {
    338                 mVerticalEdges.add(new Segment(b.x, b.y, b.y2(), node, id, LEFT, NO_MARGIN));
    339             }
    340 
    341             if (margins.right != 0) {
    342                 mVerticalEdges.add(new Segment(b.x2(), b.y, b.y2(), node, id,
    343                         RIGHT, WITHOUT_MARGIN));
    344                 mVerticalEdges.add(new Segment(b.x2() + margins.right, b.y, b.y2(), node, id,
    345                         RIGHT, WITH_MARGIN));
    346             } else {
    347                 mVerticalEdges.add(new Segment(b.x2(), b.y, b.y2(), node, id,
    348                         RIGHT, NO_MARGIN));
    349             }
    350         }
    351     }
    352 
    353     /** Records the center edges for the given node to the potential match list */
    354     protected void addCenter(INode node, String id,
    355             boolean addHorizontal, boolean addVertical) {
    356         Rect b = node.getBounds();
    357 
    358         if (addHorizontal) {
    359             mCenterHorizEdges.add(new Segment(b.centerY(), b.x, b.x2(),
    360                 node, id, CENTER_HORIZONTAL, NO_MARGIN));
    361         }
    362         if (addVertical) {
    363             mCenterVertEdges.add(new Segment(b.centerX(), b.y, b.y2(),
    364                 node, id, CENTER_VERTICAL, NO_MARGIN));
    365         }
    366     }
    367 
    368     /** Records the baseline edge for the given node to the potential match list */
    369     protected int addBaseLine(INode node, String id) {
    370         int baselineY = node.getBaseline();
    371         if (baselineY != -1) {
    372             Rect b = node.getBounds();
    373             mHorizontalEdges.add(new Segment(b.y + baselineY, b.x, b.x2(), node, id, BASELINE,
    374                     NO_MARGIN));
    375         }
    376 
    377         return baselineY;
    378     }
    379 
    380     protected void snapVertical(Segment vEdge, int x, Rect newBounds) {
    381         newBounds.x = x;
    382     }
    383 
    384     protected void snapHorizontal(Segment hEdge, int y, Rect newBounds) {
    385         newBounds.y = y;
    386     }
    387 
    388     /**
    389      * Returns whether two edge types are compatible. For example, we only match the
    390      * center of one object with the center of another.
    391      *
    392      * @param edge the first edge type to compare
    393      * @param dragged the second edge type to compare the first one with
    394      * @param delta the delta between the two edge locations
    395      * @return true if the two edge types can be compatibly matched
    396      */
    397     protected boolean isEdgeTypeCompatible(SegmentType edge, SegmentType dragged, int delta) {
    398 
    399         if (Math.abs(delta) > BaseLayoutRule.getMaxMatchDistance()) {
    400             if (dragged == LEFT || dragged == TOP) {
    401                 if (delta > 0) {
    402                     return false;
    403                 }
    404             } else {
    405                 if (delta < 0) {
    406                     return false;
    407                 }
    408             }
    409         }
    410 
    411         switch (edge) {
    412             case BOTTOM:
    413             case TOP:
    414                 return dragged == TOP || dragged == BOTTOM;
    415             case LEFT:
    416             case RIGHT:
    417                 return dragged == LEFT || dragged == RIGHT;
    418 
    419             // Center horizontal, center vertical and Baseline only matches the same
    420             // type, and only within the matching distance -- no margins!
    421             case BASELINE:
    422             case CENTER_HORIZONTAL:
    423             case CENTER_VERTICAL:
    424                 return dragged == edge && Math.abs(delta) < getMaxMatchDistance();
    425             default: assert false : edge;
    426         }
    427         return false;
    428     }
    429 
    430     /**
    431      * Finds the closest matching segments among the given list of edges for the given
    432      * dragged edge, and returns these as a list of matches
    433      */
    434     protected List<Match> findClosest(Segment draggedEdge, List<Segment> edges) {
    435         List<Match> closest = new ArrayList<Match>();
    436         addClosest(draggedEdge, edges, closest);
    437         return closest;
    438     }
    439 
    440     protected void addClosest(Segment draggedEdge, List<Segment> edges,
    441             List<Match> closest) {
    442         int at = draggedEdge.at;
    443         int closestDelta = closest.size() > 0 ? closest.get(0).delta : Integer.MAX_VALUE;
    444         int closestDistance = abs(closestDelta);
    445         for (Segment edge : edges) {
    446             assert draggedEdge.edgeType.isHorizontal() == edge.edgeType.isHorizontal();
    447 
    448             int delta = edge.at - at;
    449             int distance = abs(delta);
    450             if (distance > closestDistance) {
    451                 continue;
    452             }
    453 
    454             if (!isEdgeTypeCompatible(edge.edgeType, draggedEdge.edgeType, delta)) {
    455                 continue;
    456             }
    457 
    458             boolean withParent = edge.node == layout;
    459             ConstraintType type = ConstraintType.forMatch(withParent,
    460                     draggedEdge.edgeType, edge.edgeType);
    461             if (type == null) {
    462                 continue;
    463             }
    464 
    465             // Ensure that the edge match is compatible; for example, a "below"
    466             // constraint can only apply to the margin bounds and a "bottom"
    467             // constraint can only apply to the non-margin bounds.
    468             if (type.relativeToMargin && edge.marginType == WITHOUT_MARGIN) {
    469                 continue;
    470             } else if (!type.relativeToMargin && edge.marginType == WITH_MARGIN) {
    471                 continue;
    472             }
    473 
    474             Match match = new Match(this, edge, draggedEdge, type, delta);
    475 
    476             if (distance < closestDistance) {
    477                 closest.clear();
    478                 closestDistance = distance;
    479                 closestDelta = delta;
    480             } else if (delta * closestDelta < 0) {
    481                 // They have different signs, e.g. the matches are equal but
    482                 // on opposite sides; can't accept them both
    483                 continue;
    484             }
    485             closest.add(match);
    486         }
    487     }
    488 
    489     protected void clearSuggestions() {
    490         mHorizontalSuggestions = mVerticalSuggestions = null;
    491         mCurrentLeftMatch = mCurrentRightMatch = null;
    492         mCurrentTopMatch = mCurrentBottomMatch = null;
    493     }
    494 
    495     /**
    496      * Given a node, apply the suggestions by expressing them as relative layout param
    497      * values
    498      *
    499      * @param n the node to apply constraints to
    500      */
    501     public void applyConstraints(INode n) {
    502         // Process each edge separately
    503         String centerBoth = n.getStringAttr(ANDROID_URI, ATTR_LAYOUT_CENTER_IN_PARENT);
    504         if (centerBoth != null && centerBoth.equals(VALUE_TRUE)) {
    505             n.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_IN_PARENT, null);
    506 
    507             // If you had a center-in-both-directions attribute, and you're
    508             // only resizing in one dimension, then leave the other dimension
    509             // centered, e.g. if you have centerInParent and apply alignLeft,
    510             // then you should end up with alignLeft and centerVertically
    511             if (mCurrentTopMatch == null && mCurrentBottomMatch == null) {
    512                 n.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_VERTICAL, VALUE_TRUE);
    513             }
    514             if (mCurrentLeftMatch == null && mCurrentRightMatch == null) {
    515                 n.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_HORIZONTAL, VALUE_TRUE);
    516             }
    517         }
    518 
    519         if (mMoveTop) {
    520             // Remove top attachments
    521             n.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_PARENT_TOP, null);
    522             n.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_TOP, null);
    523             n.setAttribute(ANDROID_URI, ATTR_LAYOUT_BELOW, null);
    524 
    525             n.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_VERTICAL, null);
    526             n.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_BASELINE, null);
    527 
    528         }
    529 
    530         if (mMoveBottom) {
    531             // Remove bottom attachments
    532             n.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_PARENT_BOTTOM, null);
    533             n.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_BOTTOM, null);
    534             n.setAttribute(ANDROID_URI, ATTR_LAYOUT_ABOVE, null);
    535             n.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_VERTICAL, null);
    536             n.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_BASELINE, null);
    537         }
    538 
    539         if (mMoveLeft) {
    540             // Remove left attachments
    541             n.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_PARENT_LEFT, null);
    542             n.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_LEFT, null);
    543             n.setAttribute(ANDROID_URI, ATTR_LAYOUT_TO_RIGHT_OF, null);
    544             n.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_HORIZONTAL, null);
    545         }
    546 
    547         if (mMoveRight) {
    548             // Remove right attachments
    549             n.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_PARENT_RIGHT, null);
    550             n.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_RIGHT, null);
    551             n.setAttribute(ANDROID_URI, ATTR_LAYOUT_TO_LEFT_OF, null);
    552             n.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_HORIZONTAL, null);
    553         }
    554 
    555         if (mMoveTop && mCurrentTopMatch != null) {
    556             applyConstraint(n, mCurrentTopMatch.getConstraint(true /* generateId */));
    557             if (mCurrentTopMatch.type == ALIGN_BASELINE) {
    558                 // HACK! WORKAROUND! Baseline doesn't provide a new bottom edge for attachments
    559                 String c = mCurrentTopMatch.getConstraint(true);
    560                 c = c.replace(ATTR_LAYOUT_ALIGN_BASELINE, ATTR_LAYOUT_ALIGN_BOTTOM);
    561                 applyConstraint(n, c);
    562             }
    563         }
    564 
    565         if (mMoveBottom && mCurrentBottomMatch != null) {
    566             applyConstraint(n, mCurrentBottomMatch.getConstraint(true));
    567         }
    568 
    569         if (mMoveLeft && mCurrentLeftMatch != null) {
    570             applyConstraint(n, mCurrentLeftMatch.getConstraint(true));
    571         }
    572 
    573         if (mMoveRight && mCurrentRightMatch != null) {
    574             applyConstraint(n, mCurrentRightMatch.getConstraint(true));
    575         }
    576 
    577         if (mMoveLeft) {
    578             applyMargin(n, ATTR_LAYOUT_MARGIN_LEFT, mLeftMargin);
    579         }
    580         if (mMoveRight) {
    581             applyMargin(n, ATTR_LAYOUT_MARGIN_RIGHT, mRightMargin);
    582         }
    583         if (mMoveTop) {
    584             applyMargin(n, ATTR_LAYOUT_MARGIN_TOP, mTopMargin);
    585         }
    586         if (mMoveBottom) {
    587             applyMargin(n, ATTR_LAYOUT_MARGIN_BOTTOM, mBottomMargin);
    588         }
    589     }
    590 
    591     private void applyConstraint(INode n, String constraint) {
    592         assert constraint.contains("=") : constraint;
    593         String name = constraint.substring(0, constraint.indexOf('='));
    594         String value = constraint.substring(constraint.indexOf('=') + 1);
    595         n.setAttribute(ANDROID_URI, name, value);
    596     }
    597 
    598     private void applyMargin(INode n, String marginAttribute, int margin) {
    599         if (margin > 0) {
    600             int dp = mRulesEngine.pxToDp(margin);
    601             n.setAttribute(ANDROID_URI, marginAttribute, String.format(VALUE_N_DP, dp));
    602         } else if (n.getStringAttr(ANDROID_URI, marginAttribute) != null) {
    603             // Clear out existing margin
    604             n.setAttribute(ANDROID_URI, marginAttribute, null);
    605         }
    606     }
    607 
    608     private void removeRelativeParams(INode node) {
    609         for (ConstraintType type : ConstraintType.values()) {
    610             node.setAttribute(ANDROID_URI, type.name, null);
    611         }
    612         node.setAttribute(ANDROID_URI,ATTR_LAYOUT_MARGIN_LEFT, null);
    613         node.setAttribute(ANDROID_URI,ATTR_LAYOUT_MARGIN_RIGHT, null);
    614         node.setAttribute(ANDROID_URI,ATTR_LAYOUT_MARGIN_TOP, null);
    615         node.setAttribute(ANDROID_URI,ATTR_LAYOUT_MARGIN_BOTTOM, null);
    616     }
    617 
    618     /**
    619      * Attach the new child to the previous node
    620      * @param previous the previous child
    621      * @param node the new child to attach it to
    622      */
    623     public void attachPrevious(INode previous, INode node) {
    624         removeRelativeParams(node);
    625 
    626         String id = previous.getStringAttr(ANDROID_URI, ATTR_ID);
    627         if (id == null) {
    628             return;
    629         }
    630 
    631         if (mCurrentTopMatch != null || mCurrentBottomMatch != null) {
    632             // Attaching the top: arrange below, and for bottom arrange above
    633             node.setAttribute(ANDROID_URI,
    634                     mCurrentTopMatch != null ? ATTR_LAYOUT_BELOW : ATTR_LAYOUT_ABOVE, id);
    635             // Apply same left/right constraints as the parent
    636             if (mCurrentLeftMatch != null) {
    637                 applyConstraint(node, mCurrentLeftMatch.getConstraint(true));
    638                 applyMargin(node, ATTR_LAYOUT_MARGIN_LEFT, mLeftMargin);
    639             } else if (mCurrentRightMatch != null) {
    640                 applyConstraint(node, mCurrentRightMatch.getConstraint(true));
    641                 applyMargin(node, ATTR_LAYOUT_MARGIN_RIGHT, mRightMargin);
    642             }
    643         } else if (mCurrentLeftMatch != null || mCurrentRightMatch != null) {
    644             node.setAttribute(ANDROID_URI,
    645                     mCurrentLeftMatch != null ? ATTR_LAYOUT_TO_RIGHT_OF : ATTR_LAYOUT_TO_LEFT_OF,
    646                             id);
    647             // Apply same top/bottom constraints as the parent
    648             if (mCurrentTopMatch != null) {
    649                 applyConstraint(node, mCurrentTopMatch.getConstraint(true));
    650                 applyMargin(node, ATTR_LAYOUT_MARGIN_TOP, mTopMargin);
    651             } else if (mCurrentBottomMatch != null) {
    652                 applyConstraint(node, mCurrentBottomMatch.getConstraint(true));
    653                 applyMargin(node, ATTR_LAYOUT_MARGIN_BOTTOM, mBottomMargin);
    654             }
    655         } else {
    656             return;
    657         }
    658     }
    659 
    660     public void removeCycles() {
    661         if (mHorizontalCycle != null) {
    662             removeCycles(mHorizontalDeps);
    663         }
    664         if (mVerticalCycle != null) {
    665             removeCycles(mVerticalDeps);
    666         }
    667     }
    668 
    669     private void removeCycles(Set<INode> deps) {
    670         for (INode node : mDraggedNodes) {
    671             ViewData view = mDependencyGraph.getView(node);
    672             if (view != null) {
    673                 for (Constraint constraint : view.dependedOnBy) {
    674                     // For now, remove ALL constraints pointing to this node in this orientation.
    675                     // Later refine this to be smarter. (We can't JUST remove the constraints
    676                     // identified in the cycle since there could be multiple.)
    677                     constraint.from.node.setAttribute(ANDROID_URI, constraint.type.name, null);
    678                 }
    679             }
    680         }
    681     }
    682 
    683     /**
    684      * Comparator used to sort matches such that the first match is the most desirable
    685      * match (where we prefer attaching to parent bounds, we avoid matches that lead to a
    686      * cycle, we prefer constraints on closer widgets rather than ones further away, and
    687      * so on.)
    688      * <p>
    689      * There are a number of sorting criteria. One of them is the distance between the
    690      * matched edges. We may end up with multiple matches that are the same distance. In
    691      * that case we look at the orientation; on the left side, prefer left-oriented
    692      * attachments, and on the right-side prefer right-oriented attachments. For example,
    693      * consider the following scenario:
    694      *
    695      * <pre>
    696      *    +--------------------+-------------------------+
    697      *    | Attached on left   |                         |
    698      *    +--------------------+                         |
    699      *    |                                              |
    700      *    |                    +-----+                   |
    701      *    |                    |  A  |                   |
    702      *    |                    +-----+                   |
    703      *    |                                              |
    704      *    |                    +-------------------------+
    705      *    |                    |       Attached on right |
    706      *    +--------------------+-------------------------+
    707      * </pre>
    708      *
    709      * Here, dragging the left edge should attach to the top left attached view, whereas
    710      * in the following layout dragging the right edge would attach to the bottom view:
    711      *
    712      * <pre>
    713      *    +--------------------------+-------------------+
    714      *    | Attached on left         |                   |
    715      *    +--------------------------+                   |
    716      *    |                                              |
    717      *    |                    +-----+                   |
    718      *    |                    |  A  |                   |
    719      *    |                    +-----+                   |
    720      *    |                                              |
    721      *    |                          +-------------------+
    722      *    |                          | Attached on right |
    723      *    +--------------------------+-------------------+
    724      *
    725      * </pre>
    726      *
    727      * </ul>
    728      */
    729     private final class MatchComparator implements Comparator<Match> {
    730         public int compare(Match m1, Match m2) {
    731             // Always prefer matching parent bounds
    732             int parent1 = m1.edge.node == layout ? -1 : 1;
    733             int parent2 = m2.edge.node == layout ? -1 : 1;
    734             // unless it's a center bound -- those should always get lowest priority since
    735             // they overlap with other usually more interesting edges near the center of
    736             // the layout.
    737             if (m1.edge.edgeType == CENTER_HORIZONTAL
    738                     || m1.edge.edgeType == CENTER_VERTICAL) {
    739                 parent1 = 2;
    740             }
    741             if (m2.edge.edgeType == CENTER_HORIZONTAL
    742                     || m2.edge.edgeType == CENTER_VERTICAL) {
    743                 parent2 = 2;
    744             }
    745             if (parent1 != parent2) {
    746                 return parent1 - parent2;
    747             }
    748 
    749             // Avoid matching edges that would lead to a cycle
    750             if (m1.edge.edgeType.isHorizontal()) {
    751                 int cycle1 = mHorizontalDeps.contains(m1.edge.node) ? 1 : -1;
    752                 int cycle2 = mHorizontalDeps.contains(m2.edge.node) ? 1 : -1;
    753                 if (cycle1 != cycle2) {
    754                     return cycle1 - cycle2;
    755                 }
    756             } else {
    757                 int cycle1 = mVerticalDeps.contains(m1.edge.node) ? 1 : -1;
    758                 int cycle2 = mVerticalDeps.contains(m2.edge.node) ? 1 : -1;
    759                 if (cycle1 != cycle2) {
    760                     return cycle1 - cycle2;
    761                 }
    762             }
    763 
    764             // TODO: Sort by minimum depth -- do we have the depth anywhere?
    765 
    766             // Prefer nodes that are closer
    767             int distance1, distance2;
    768             if (m1.edge.to <= m1.with.from) {
    769                 distance1 = m1.with.from - m1.edge.to;
    770             } else if (m1.edge.from >= m1.with.to) {
    771                 distance1 = m1.edge.from - m1.with.to;
    772             } else {
    773                 // Some kind of overlap - not sure how to prioritize these yet...
    774                 distance1 = 0;
    775             }
    776             if (m2.edge.to <= m2.with.from) {
    777                 distance2 = m2.with.from - m2.edge.to;
    778             } else if (m2.edge.from >= m2.with.to) {
    779                 distance2 = m2.edge.from - m2.with.to;
    780             } else {
    781                 // Some kind of overlap - not sure how to prioritize these yet...
    782                 distance2 = 0;
    783             }
    784 
    785             if (distance1 != distance2) {
    786                 return distance1 - distance2;
    787             }
    788 
    789             // Prefer matching on baseline
    790             int baseline1 = (m1.edge.edgeType == BASELINE) ? -1 : 1;
    791             int baseline2 = (m2.edge.edgeType == BASELINE) ? -1 : 1;
    792             if (baseline1 != baseline2) {
    793                 return baseline1 - baseline2;
    794             }
    795 
    796             // Prefer matching top/left edges before matching bottom/right edges
    797             int orientation1 = (m1.with.edgeType == LEFT ||
    798                       m1.with.edgeType == TOP) ? -1 : 1;
    799             int orientation2 = (m2.with.edgeType == LEFT ||
    800                       m2.with.edgeType == TOP) ? -1 : 1;
    801             if (orientation1 != orientation2) {
    802                 return orientation1 - orientation2;
    803             }
    804 
    805             // Prefer opposite-matching over same-matching.
    806             // In other words, if we have the choice of matching
    807             // our left edge with another element's left edge,
    808             // or matching our left edge with another element's right
    809             // edge, prefer the right edge since that
    810             // The two matches have identical distance; try to sort by
    811             // orientation
    812             int edgeType1 = (m1.edge.edgeType != m1.with.edgeType) ? -1 : 1;
    813             int edgeType2 = (m2.edge.edgeType != m2.with.edgeType) ? -1 : 1;
    814             if (edgeType1 != edgeType2) {
    815                 return edgeType1 - edgeType2;
    816             }
    817 
    818             return 0;
    819         }
    820     }
    821 
    822     /**
    823      * Returns the {@link IClientRulesEngine} IDE callback
    824      *
    825      * @return the {@link IClientRulesEngine} IDE callback, never null
    826      */
    827     public IClientRulesEngine getRulesEngine() {
    828         return mRulesEngine;
    829     }
    830 }