• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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      */
GuidelineHandler(INode layout, IClientRulesEngine rulesEngine)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      */
haveSuggestions()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      */
pickBestMatch(List<Match> matches)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 
checkCycle(DropFeedback feedback, Match match, boolean vertical)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 
checkCycles(DropFeedback feedback)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 */
addBounds(INode node, String id, boolean addHorizontal, boolean addVertical)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 */
addCenter(INode node, String id, boolean addHorizontal, boolean addVertical)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 */
addBaseLine(INode node, String id)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 
snapVertical(Segment vEdge, int x, Rect newBounds)380     protected void snapVertical(Segment vEdge, int x, Rect newBounds) {
381         newBounds.x = x;
382     }
383 
snapHorizontal(Segment hEdge, int y, Rect newBounds)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      */
isEdgeTypeCompatible(SegmentType edge, SegmentType dragged, int delta)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      */
findClosest(Segment draggedEdge, List<Segment> edges)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 
addClosest(Segment draggedEdge, List<Segment> edges, List<Match> closest)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 
clearSuggestions()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      */
applyConstraints(INode n)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 
applyConstraint(INode n, String constraint)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 
applyMargin(INode n, String marginAttribute, int margin)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 
removeRelativeParams(INode node)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      */
attachPrevious(INode previous, INode node)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 
removeCycles()660     public void removeCycles() {
661         if (mHorizontalCycle != null) {
662             removeCycles(mHorizontalDeps);
663         }
664         if (mVerticalCycle != null) {
665             removeCycles(mVerticalDeps);
666         }
667     }
668 
removeCycles(Set<INode> deps)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         @Override
compare(Match m1, Match m2)731         public int compare(Match m1, Match m2) {
732             // Always prefer matching parent bounds
733             int parent1 = m1.edge.node == layout ? -1 : 1;
734             int parent2 = m2.edge.node == layout ? -1 : 1;
735             // unless it's a center bound -- those should always get lowest priority since
736             // they overlap with other usually more interesting edges near the center of
737             // the layout.
738             if (m1.edge.edgeType == CENTER_HORIZONTAL
739                     || m1.edge.edgeType == CENTER_VERTICAL) {
740                 parent1 = 2;
741             }
742             if (m2.edge.edgeType == CENTER_HORIZONTAL
743                     || m2.edge.edgeType == CENTER_VERTICAL) {
744                 parent2 = 2;
745             }
746             if (parent1 != parent2) {
747                 return parent1 - parent2;
748             }
749 
750             // Avoid matching edges that would lead to a cycle
751             if (m1.edge.edgeType.isHorizontal()) {
752                 int cycle1 = mHorizontalDeps.contains(m1.edge.node) ? 1 : -1;
753                 int cycle2 = mHorizontalDeps.contains(m2.edge.node) ? 1 : -1;
754                 if (cycle1 != cycle2) {
755                     return cycle1 - cycle2;
756                 }
757             } else {
758                 int cycle1 = mVerticalDeps.contains(m1.edge.node) ? 1 : -1;
759                 int cycle2 = mVerticalDeps.contains(m2.edge.node) ? 1 : -1;
760                 if (cycle1 != cycle2) {
761                     return cycle1 - cycle2;
762                 }
763             }
764 
765             // TODO: Sort by minimum depth -- do we have the depth anywhere?
766 
767             // Prefer nodes that are closer
768             int distance1, distance2;
769             if (m1.edge.to <= m1.with.from) {
770                 distance1 = m1.with.from - m1.edge.to;
771             } else if (m1.edge.from >= m1.with.to) {
772                 distance1 = m1.edge.from - m1.with.to;
773             } else {
774                 // Some kind of overlap - not sure how to prioritize these yet...
775                 distance1 = 0;
776             }
777             if (m2.edge.to <= m2.with.from) {
778                 distance2 = m2.with.from - m2.edge.to;
779             } else if (m2.edge.from >= m2.with.to) {
780                 distance2 = m2.edge.from - m2.with.to;
781             } else {
782                 // Some kind of overlap - not sure how to prioritize these yet...
783                 distance2 = 0;
784             }
785 
786             if (distance1 != distance2) {
787                 return distance1 - distance2;
788             }
789 
790             // Prefer matching on baseline
791             int baseline1 = (m1.edge.edgeType == BASELINE) ? -1 : 1;
792             int baseline2 = (m2.edge.edgeType == BASELINE) ? -1 : 1;
793             if (baseline1 != baseline2) {
794                 return baseline1 - baseline2;
795             }
796 
797             // Prefer matching top/left edges before matching bottom/right edges
798             int orientation1 = (m1.with.edgeType == LEFT ||
799                       m1.with.edgeType == TOP) ? -1 : 1;
800             int orientation2 = (m2.with.edgeType == LEFT ||
801                       m2.with.edgeType == TOP) ? -1 : 1;
802             if (orientation1 != orientation2) {
803                 return orientation1 - orientation2;
804             }
805 
806             // Prefer opposite-matching over same-matching.
807             // In other words, if we have the choice of matching
808             // our left edge with another element's left edge,
809             // or matching our left edge with another element's right
810             // edge, prefer the right edge since that
811             // The two matches have identical distance; try to sort by
812             // orientation
813             int edgeType1 = (m1.edge.edgeType != m1.with.edgeType) ? -1 : 1;
814             int edgeType2 = (m2.edge.edgeType != m2.with.edgeType) ? -1 : 1;
815             if (edgeType1 != edgeType2) {
816                 return edgeType1 - edgeType2;
817             }
818 
819             return 0;
820         }
821     }
822 
823     /**
824      * Returns the {@link IClientRulesEngine} IDE callback
825      *
826      * @return the {@link IClientRulesEngine} IDE callback, never null
827      */
getRulesEngine()828     public IClientRulesEngine getRulesEngine() {
829         return mRulesEngine;
830     }
831 }