• 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.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      */
GuidelineHandler(INode layout, IClientRulesEngine rulesEngine)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      */
haveSuggestions()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      */
pickBestMatch(List<Match> matches)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 
checkCycle(DropFeedback feedback, Match match, boolean vertical)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      */
checkCycles(DropFeedback feedback)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 */
addBounds(INode node, String id, boolean addHorizontal, boolean addVertical)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 */
addCenter(INode node, String id, boolean addHorizontal, boolean addVertical)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 */
addBaseLine(INode node, String id)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 
snapVertical(Segment vEdge, int x, Rect newBounds)387     protected void snapVertical(Segment vEdge, int x, Rect newBounds) {
388         newBounds.x = x;
389     }
390 
snapHorizontal(Segment hEdge, int y, Rect newBounds)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      */
isEdgeTypeCompatible(SegmentType edge, SegmentType dragged, int delta)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      */
findClosest(Segment draggedEdge, List<Segment> edges)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 
addClosest(Segment draggedEdge, List<Segment> edges, List<Match> closest)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 
clearSuggestions()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      */
applyConstraints(INode n)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 
applyConstraint(INode n, String constraint)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 
applyMargin(INode n, String marginAttribute, int margin)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 
removeRelativeParams(INode node)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      */
attachPrevious(INode previous, INode node)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 */
removeCycles()668     public void removeCycles() {
669         if (mHorizontalCycle != null) {
670             removeCycles(mHorizontalDeps);
671         }
672         if (mVerticalCycle != null) {
673             removeCycles(mVerticalDeps);
674         }
675     }
676 
removeCycles(Set<INode> deps)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
compare(Match m1, Match m2)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      */
getRulesEngine()836     public IClientRulesEngine getRulesEngine() {
837         return mRulesEngine;
838     }
839 }
840