• Home
  • History
  • Annotate
  • 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