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