aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/GuidelineHandler.java
diff options
context:
space:
mode:
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/GuidelineHandler.java')
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/GuidelineHandler.java839
1 files changed, 839 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/GuidelineHandler.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/GuidelineHandler.java
new file mode 100644
index 000000000..db08b1857
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/GuidelineHandler.java
@@ -0,0 +1,839 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.common.layout.relative;
+
+import static com.android.ide.common.api.MarginType.NO_MARGIN;
+import static com.android.ide.common.api.MarginType.WITHOUT_MARGIN;
+import static com.android.ide.common.api.MarginType.WITH_MARGIN;
+import static com.android.ide.common.api.SegmentType.BASELINE;
+import static com.android.ide.common.api.SegmentType.BOTTOM;
+import static com.android.ide.common.api.SegmentType.CENTER_HORIZONTAL;
+import static com.android.ide.common.api.SegmentType.CENTER_VERTICAL;
+import static com.android.ide.common.api.SegmentType.LEFT;
+import static com.android.ide.common.api.SegmentType.RIGHT;
+import static com.android.ide.common.api.SegmentType.TOP;
+import static com.android.ide.common.layout.BaseLayoutRule.getMaxMatchDistance;
+import static com.android.SdkConstants.ATTR_ID;
+import static com.android.SdkConstants.ATTR_LAYOUT_ABOVE;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_BASELINE;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_BOTTOM;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_LEFT;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_BOTTOM;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_LEFT;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_RIGHT;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_TOP;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_RIGHT;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_TOP;
+import static com.android.SdkConstants.ATTR_LAYOUT_BELOW;
+import static com.android.SdkConstants.ATTR_LAYOUT_CENTER_HORIZONTAL;
+import static com.android.SdkConstants.ATTR_LAYOUT_CENTER_IN_PARENT;
+import static com.android.SdkConstants.ATTR_LAYOUT_CENTER_VERTICAL;
+import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_BOTTOM;
+import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_LEFT;
+import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_RIGHT;
+import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_TOP;
+import static com.android.SdkConstants.ATTR_LAYOUT_TO_LEFT_OF;
+import static com.android.SdkConstants.ATTR_LAYOUT_TO_RIGHT_OF;
+import static com.android.SdkConstants.VALUE_N_DP;
+import static com.android.SdkConstants.VALUE_TRUE;
+import static com.android.ide.common.layout.relative.ConstraintType.ALIGN_BASELINE;
+
+import static java.lang.Math.abs;
+
+import com.android.SdkConstants;
+import static com.android.SdkConstants.ANDROID_URI;
+import com.android.ide.common.api.DropFeedback;
+import com.android.ide.common.api.IClientRulesEngine;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.Margins;
+import com.android.ide.common.api.Rect;
+import com.android.ide.common.api.Segment;
+import com.android.ide.common.api.SegmentType;
+import com.android.ide.common.layout.BaseLayoutRule;
+import com.android.ide.common.layout.relative.DependencyGraph.Constraint;
+import com.android.ide.common.layout.relative.DependencyGraph.ViewData;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * The {@link GuidelineHandler} class keeps track of state related to a guideline operation
+ * like move and resize, and performs various constraint computations.
+ */
+public class GuidelineHandler {
+ /**
+ * A dependency graph for the relative layout recording constraint relationships
+ */
+ protected DependencyGraph mDependencyGraph;
+
+ /** The RelativeLayout we are moving/resizing within */
+ public INode layout;
+
+ /** The set of nodes being dragged (may be null) */
+ protected Collection<INode> mDraggedNodes;
+
+ /** The bounds of the primary child node being dragged */
+ protected Rect mBounds;
+
+ /** Whether the left edge is being moved/resized */
+ protected boolean mMoveLeft;
+
+ /** Whether the right edge is being moved/resized */
+ protected boolean mMoveRight;
+
+ /** Whether the top edge is being moved/resized */
+ protected boolean mMoveTop;
+
+ /** Whether the bottom edge is being moved/resized */
+ protected boolean mMoveBottom;
+
+ /**
+ * Whether the drop/move/resize position should be snapped (which can be turned off
+ * with a modifier key during the operation)
+ */
+ protected boolean mSnap = true;
+
+ /**
+ * The set of nodes which depend on the currently selected nodes, including
+ * transitively, through horizontal constraints (a "horizontal constraint"
+ * is a constraint between two horizontal edges)
+ */
+ protected Set<INode> mHorizontalDeps;
+
+ /**
+ * The set of nodes which depend on the currently selected nodes, including
+ * transitively, through vertical constraints (a "vertical constraint"
+ * is a constraint between two vertical edges)
+ */
+ protected Set<INode> mVerticalDeps;
+
+ /** The current list of constraints which result in a horizontal cycle (if applicable) */
+ protected List<Constraint> mHorizontalCycle;
+
+ /** The current list of constraints which result in a vertical cycle (if applicable) */
+ protected List<Constraint> mVerticalCycle;
+
+ /**
+ * All horizontal segments in the relative layout - top and bottom edges, baseline
+ * edges, and top and bottom edges offset by the applicable margins in each direction
+ */
+ protected List<Segment> mHorizontalEdges;
+
+ /**
+ * All vertical segments in the relative layout - left and right edges, and left and
+ * right edges offset by the applicable margins in each direction
+ */
+ protected List<Segment> mVerticalEdges;
+
+ /**
+ * All center vertical segments in the relative layout. These are kept separate since
+ * they only match other center edges.
+ */
+ protected List<Segment> mCenterVertEdges;
+
+ /**
+ * All center horizontal segments in the relative layout. These are kept separate
+ * since they only match other center edges.
+ */
+ protected List<Segment> mCenterHorizEdges;
+
+ /**
+ * Suggestions for horizontal matches. There could be more than one, but all matches
+ * will be equidistant from the current position (as well as in the same direction,
+ * which means that you can't have one match 5 pixels to the left and one match 5
+ * pixels to the right since it would be impossible to snap to fit with both; you can
+ * however have multiple matches all 5 pixels to the left.)
+ * <p
+ * The best vertical match will be found in {@link #mCurrentTopMatch} or
+ * {@link #mCurrentBottomMatch}.
+ */
+ protected List<Match> mHorizontalSuggestions;
+
+ /**
+ * Suggestions for vertical matches.
+ * <p
+ * The best vertical match will be found in {@link #mCurrentLeftMatch} or
+ * {@link #mCurrentRightMatch}.
+ */
+ protected List<Match> mVerticalSuggestions;
+
+ /**
+ * The current match on the left edge, or null if no match or if the left edge is not
+ * being moved or resized.
+ */
+ protected Match mCurrentLeftMatch;
+
+ /**
+ * The current match on the top edge, or null if no match or if the top edge is not
+ * being moved or resized.
+ */
+ protected Match mCurrentTopMatch;
+
+ /**
+ * The current match on the right edge, or null if no match or if the right edge is
+ * not being moved or resized.
+ */
+ protected Match mCurrentRightMatch;
+
+ /**
+ * The current match on the bottom edge, or null if no match or if the bottom edge is
+ * not being moved or resized.
+ */
+ protected Match mCurrentBottomMatch;
+
+ /**
+ * The amount of margin to add to the top edge, or 0
+ */
+ protected int mTopMargin;
+
+ /**
+ * The amount of margin to add to the bottom edge, or 0
+ */
+ protected int mBottomMargin;
+
+ /**
+ * The amount of margin to add to the left edge, or 0
+ */
+ protected int mLeftMargin;
+
+ /**
+ * The amount of margin to add to the right edge, or 0
+ */
+ protected int mRightMargin;
+
+ /**
+ * The associated rules engine
+ */
+ protected IClientRulesEngine mRulesEngine;
+
+ /**
+ * Construct a new {@link GuidelineHandler} for the given relative layout.
+ *
+ * @param layout the RelativeLayout to handle
+ */
+ GuidelineHandler(INode layout, IClientRulesEngine rulesEngine) {
+ this.layout = layout;
+ mRulesEngine = rulesEngine;
+
+ mHorizontalEdges = new ArrayList<Segment>();
+ mVerticalEdges = new ArrayList<Segment>();
+ mCenterVertEdges = new ArrayList<Segment>();
+ mCenterHorizEdges = new ArrayList<Segment>();
+ mDependencyGraph = new DependencyGraph(layout);
+ }
+
+ /**
+ * Returns true if the handler has any suggestions to offer
+ *
+ * @return true if the handler has any suggestions to offer
+ */
+ public boolean haveSuggestions() {
+ return mCurrentLeftMatch != null || mCurrentTopMatch != null
+ || mCurrentRightMatch != null || mCurrentBottomMatch != null;
+ }
+
+ /**
+ * Returns the closest match.
+ *
+ * @return the closest match, or null if nothing matched
+ */
+ protected Match pickBestMatch(List<Match> matches) {
+ int alternatives = matches.size();
+ if (alternatives == 0) {
+ return null;
+ } else if (alternatives == 1) {
+ Match match = matches.get(0);
+ return match;
+ } else {
+ assert alternatives > 1;
+ Collections.sort(matches, new MatchComparator());
+ return matches.get(0);
+ }
+ }
+
+ private boolean checkCycle(DropFeedback feedback, Match match, boolean vertical) {
+ if (match != null && match.cycle) {
+ for (INode node : mDraggedNodes) {
+ INode from = match.edge.node;
+ assert match.with.node == null || match.with.node == node;
+ INode to = node;
+ List<Constraint> path = mDependencyGraph.getPathTo(from, to, vertical);
+ if (path != null) {
+ if (vertical) {
+ mVerticalCycle = path;
+ } else {
+ mHorizontalCycle = path;
+ }
+ String desc = Constraint.describePath(path,
+ match.type.name, match.edge.id);
+
+ feedback.errorMessage = "Constraint creates a cycle: " + desc;
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Checks for any cycles in the dependencies
+ *
+ * @param feedback the drop feedback state
+ */
+ public void checkCycles(DropFeedback feedback) {
+ // Deliberate short circuit evaluation -- only list the first cycle
+ feedback.errorMessage = null;
+ mHorizontalCycle = null;
+ mVerticalCycle = null;
+
+ if (checkCycle(feedback, mCurrentTopMatch, true /* vertical */)
+ || checkCycle(feedback, mCurrentBottomMatch, true)) {
+ }
+
+ if (checkCycle(feedback, mCurrentLeftMatch, false)
+ || checkCycle(feedback, mCurrentRightMatch, false)) {
+ }
+ }
+
+ /** Records the matchable outside edges for the given node to the potential match list */
+ protected void addBounds(INode node, String id,
+ boolean addHorizontal, boolean addVertical) {
+ Rect b = node.getBounds();
+ Margins margins = node.getMargins();
+ if (addHorizontal) {
+ if (margins.top != 0) {
+ mHorizontalEdges.add(new Segment(b.y, b.x, b.x2(), node, id, TOP, WITHOUT_MARGIN));
+ mHorizontalEdges.add(new Segment(b.y - margins.top, b.x, b.x2(), node, id,
+ TOP, WITH_MARGIN));
+ } else {
+ mHorizontalEdges.add(new Segment(b.y, b.x, b.x2(), node, id, TOP, NO_MARGIN));
+ }
+ if (margins.bottom != 0) {
+ mHorizontalEdges.add(new Segment(b.y2(), b.x, b.x2(), node, id, BOTTOM,
+ WITHOUT_MARGIN));
+ mHorizontalEdges.add(new Segment(b.y2() + margins.bottom, b.x, b.x2(), node,
+ id, BOTTOM, WITH_MARGIN));
+ } else {
+ mHorizontalEdges.add(new Segment(b.y2(), b.x, b.x2(), node, id,
+ BOTTOM, NO_MARGIN));
+ }
+ }
+ if (addVertical) {
+ if (margins.left != 0) {
+ mVerticalEdges.add(new Segment(b.x, b.y, b.y2(), node, id, LEFT, WITHOUT_MARGIN));
+ mVerticalEdges.add(new Segment(b.x - margins.left, b.y, b.y2(), node, id, LEFT,
+ WITH_MARGIN));
+ } else {
+ mVerticalEdges.add(new Segment(b.x, b.y, b.y2(), node, id, LEFT, NO_MARGIN));
+ }
+
+ if (margins.right != 0) {
+ mVerticalEdges.add(new Segment(b.x2(), b.y, b.y2(), node, id,
+ RIGHT, WITHOUT_MARGIN));
+ mVerticalEdges.add(new Segment(b.x2() + margins.right, b.y, b.y2(), node, id,
+ RIGHT, WITH_MARGIN));
+ } else {
+ mVerticalEdges.add(new Segment(b.x2(), b.y, b.y2(), node, id,
+ RIGHT, NO_MARGIN));
+ }
+ }
+ }
+
+ /** Records the center edges for the given node to the potential match list */
+ protected void addCenter(INode node, String id,
+ boolean addHorizontal, boolean addVertical) {
+ Rect b = node.getBounds();
+
+ if (addHorizontal) {
+ mCenterHorizEdges.add(new Segment(b.centerY(), b.x, b.x2(),
+ node, id, CENTER_HORIZONTAL, NO_MARGIN));
+ }
+ if (addVertical) {
+ mCenterVertEdges.add(new Segment(b.centerX(), b.y, b.y2(),
+ node, id, CENTER_VERTICAL, NO_MARGIN));
+ }
+ }
+
+ /** Records the baseline edge for the given node to the potential match list */
+ protected int addBaseLine(INode node, String id) {
+ int baselineY = node.getBaseline();
+ if (baselineY != -1) {
+ Rect b = node.getBounds();
+ mHorizontalEdges.add(new Segment(b.y + baselineY, b.x, b.x2(), node, id, BASELINE,
+ NO_MARGIN));
+ }
+
+ return baselineY;
+ }
+
+ protected void snapVertical(Segment vEdge, int x, Rect newBounds) {
+ newBounds.x = x;
+ }
+
+ protected void snapHorizontal(Segment hEdge, int y, Rect newBounds) {
+ newBounds.y = y;
+ }
+
+ /**
+ * Returns whether two edge types are compatible. For example, we only match the
+ * center of one object with the center of another.
+ *
+ * @param edge the first edge type to compare
+ * @param dragged the second edge type to compare the first one with
+ * @param delta the delta between the two edge locations
+ * @return true if the two edge types can be compatibly matched
+ */
+ protected boolean isEdgeTypeCompatible(SegmentType edge, SegmentType dragged, int delta) {
+
+ if (Math.abs(delta) > BaseLayoutRule.getMaxMatchDistance()) {
+ if (dragged == LEFT || dragged == TOP) {
+ if (delta > 0) {
+ return false;
+ }
+ } else {
+ if (delta < 0) {
+ return false;
+ }
+ }
+ }
+
+ switch (edge) {
+ case BOTTOM:
+ case TOP:
+ return dragged == TOP || dragged == BOTTOM;
+ case LEFT:
+ case RIGHT:
+ return dragged == LEFT || dragged == RIGHT;
+
+ // Center horizontal, center vertical and Baseline only matches the same
+ // type, and only within the matching distance -- no margins!
+ case BASELINE:
+ case CENTER_HORIZONTAL:
+ case CENTER_VERTICAL:
+ return dragged == edge && Math.abs(delta) < getMaxMatchDistance();
+ default: assert false : edge;
+ }
+ return false;
+ }
+
+ /**
+ * Finds the closest matching segments among the given list of edges for the given
+ * dragged edge, and returns these as a list of matches
+ */
+ protected List<Match> findClosest(Segment draggedEdge, List<Segment> edges) {
+ List<Match> closest = new ArrayList<Match>();
+ addClosest(draggedEdge, edges, closest);
+ return closest;
+ }
+
+ protected void addClosest(Segment draggedEdge, List<Segment> edges,
+ List<Match> closest) {
+ int at = draggedEdge.at;
+ int closestDelta = closest.size() > 0 ? closest.get(0).delta : Integer.MAX_VALUE;
+ int closestDistance = abs(closestDelta);
+ for (Segment edge : edges) {
+ assert draggedEdge.edgeType.isHorizontal() == edge.edgeType.isHorizontal();
+
+ int delta = edge.at - at;
+ int distance = abs(delta);
+ if (distance > closestDistance) {
+ continue;
+ }
+
+ if (!isEdgeTypeCompatible(edge.edgeType, draggedEdge.edgeType, delta)) {
+ continue;
+ }
+
+ boolean withParent = edge.node == layout;
+ ConstraintType type = ConstraintType.forMatch(withParent,
+ draggedEdge.edgeType, edge.edgeType);
+ if (type == null) {
+ continue;
+ }
+
+ // Ensure that the edge match is compatible; for example, a "below"
+ // constraint can only apply to the margin bounds and a "bottom"
+ // constraint can only apply to the non-margin bounds.
+ if (type.relativeToMargin && edge.marginType == WITHOUT_MARGIN) {
+ continue;
+ } else if (!type.relativeToMargin && edge.marginType == WITH_MARGIN) {
+ continue;
+ }
+
+ Match match = new Match(this, edge, draggedEdge, type, delta);
+
+ if (distance < closestDistance) {
+ closest.clear();
+ closestDistance = distance;
+ closestDelta = delta;
+ } else if (delta * closestDelta < 0) {
+ // They have different signs, e.g. the matches are equal but
+ // on opposite sides; can't accept them both
+ continue;
+ }
+ closest.add(match);
+ }
+ }
+
+ protected void clearSuggestions() {
+ mHorizontalSuggestions = mVerticalSuggestions = null;
+ mCurrentLeftMatch = mCurrentRightMatch = null;
+ mCurrentTopMatch = mCurrentBottomMatch = null;
+ }
+
+ /**
+ * Given a node, apply the suggestions by expressing them as relative layout param
+ * values
+ *
+ * @param n the node to apply constraints to
+ */
+ public void applyConstraints(INode n) {
+ // Process each edge separately
+ String centerBoth = n.getStringAttr(ANDROID_URI, ATTR_LAYOUT_CENTER_IN_PARENT);
+ if (centerBoth != null && centerBoth.equals(VALUE_TRUE)) {
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_IN_PARENT, null);
+
+ // If you had a center-in-both-directions attribute, and you're
+ // only resizing in one dimension, then leave the other dimension
+ // centered, e.g. if you have centerInParent and apply alignLeft,
+ // then you should end up with alignLeft and centerVertically
+ if (mCurrentTopMatch == null && mCurrentBottomMatch == null) {
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_VERTICAL, VALUE_TRUE);
+ }
+ if (mCurrentLeftMatch == null && mCurrentRightMatch == null) {
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_HORIZONTAL, VALUE_TRUE);
+ }
+ }
+
+ if (mMoveTop) {
+ // Remove top attachments
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_PARENT_TOP, null);
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_TOP, null);
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_BELOW, null);
+
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_VERTICAL, null);
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_BASELINE, null);
+
+ }
+
+ if (mMoveBottom) {
+ // Remove bottom attachments
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_PARENT_BOTTOM, null);
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_BOTTOM, null);
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_ABOVE, null);
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_VERTICAL, null);
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_BASELINE, null);
+ }
+
+ if (mMoveLeft) {
+ // Remove left attachments
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_PARENT_LEFT, null);
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_LEFT, null);
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_TO_RIGHT_OF, null);
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_HORIZONTAL, null);
+ }
+
+ if (mMoveRight) {
+ // Remove right attachments
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_PARENT_RIGHT, null);
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_RIGHT, null);
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_TO_LEFT_OF, null);
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_HORIZONTAL, null);
+ }
+
+ if (mMoveTop && mCurrentTopMatch != null) {
+ applyConstraint(n, mCurrentTopMatch.getConstraint(true /* generateId */));
+ if (mCurrentTopMatch.type == ALIGN_BASELINE) {
+ // HACK! WORKAROUND! Baseline doesn't provide a new bottom edge for attachments
+ String c = mCurrentTopMatch.getConstraint(true);
+ c = c.replace(ATTR_LAYOUT_ALIGN_BASELINE, ATTR_LAYOUT_ALIGN_BOTTOM);
+ applyConstraint(n, c);
+ }
+ }
+
+ if (mMoveBottom && mCurrentBottomMatch != null) {
+ applyConstraint(n, mCurrentBottomMatch.getConstraint(true));
+ }
+
+ if (mMoveLeft && mCurrentLeftMatch != null) {
+ applyConstraint(n, mCurrentLeftMatch.getConstraint(true));
+ }
+
+ if (mMoveRight && mCurrentRightMatch != null) {
+ applyConstraint(n, mCurrentRightMatch.getConstraint(true));
+ }
+
+ if (mMoveLeft) {
+ applyMargin(n, ATTR_LAYOUT_MARGIN_LEFT, mLeftMargin);
+ }
+ if (mMoveRight) {
+ applyMargin(n, ATTR_LAYOUT_MARGIN_RIGHT, mRightMargin);
+ }
+ if (mMoveTop) {
+ applyMargin(n, ATTR_LAYOUT_MARGIN_TOP, mTopMargin);
+ }
+ if (mMoveBottom) {
+ applyMargin(n, ATTR_LAYOUT_MARGIN_BOTTOM, mBottomMargin);
+ }
+ }
+
+ private void applyConstraint(INode n, String constraint) {
+ assert constraint.contains("=") : constraint;
+ String name = constraint.substring(0, constraint.indexOf('='));
+ String value = constraint.substring(constraint.indexOf('=') + 1);
+ n.setAttribute(ANDROID_URI, name, value);
+ }
+
+ private void applyMargin(INode n, String marginAttribute, int margin) {
+ if (margin > 0) {
+ int dp = mRulesEngine.pxToDp(margin);
+ n.setAttribute(ANDROID_URI, marginAttribute, String.format(VALUE_N_DP, dp));
+ } else if (n.getStringAttr(ANDROID_URI, marginAttribute) != null) {
+ // Clear out existing margin
+ n.setAttribute(ANDROID_URI, marginAttribute, null);
+ }
+ }
+
+ private void removeRelativeParams(INode node) {
+ for (ConstraintType type : ConstraintType.values()) {
+ node.setAttribute(ANDROID_URI, type.name, null);
+ }
+ node.setAttribute(ANDROID_URI,ATTR_LAYOUT_MARGIN_LEFT, null);
+ node.setAttribute(ANDROID_URI,ATTR_LAYOUT_MARGIN_RIGHT, null);
+ node.setAttribute(ANDROID_URI,ATTR_LAYOUT_MARGIN_TOP, null);
+ node.setAttribute(ANDROID_URI,ATTR_LAYOUT_MARGIN_BOTTOM, null);
+ }
+
+ /**
+ * Attach the new child to the previous node
+ * @param previous the previous child
+ * @param node the new child to attach it to
+ */
+ public void attachPrevious(INode previous, INode node) {
+ removeRelativeParams(node);
+
+ String id = previous.getStringAttr(ANDROID_URI, ATTR_ID);
+ if (id == null) {
+ return;
+ }
+
+ if (mCurrentTopMatch != null || mCurrentBottomMatch != null) {
+ // Attaching the top: arrange below, and for bottom arrange above
+ node.setAttribute(ANDROID_URI,
+ mCurrentTopMatch != null ? ATTR_LAYOUT_BELOW : ATTR_LAYOUT_ABOVE, id);
+ // Apply same left/right constraints as the parent
+ if (mCurrentLeftMatch != null) {
+ applyConstraint(node, mCurrentLeftMatch.getConstraint(true));
+ applyMargin(node, ATTR_LAYOUT_MARGIN_LEFT, mLeftMargin);
+ } else if (mCurrentRightMatch != null) {
+ applyConstraint(node, mCurrentRightMatch.getConstraint(true));
+ applyMargin(node, ATTR_LAYOUT_MARGIN_RIGHT, mRightMargin);
+ }
+ } else if (mCurrentLeftMatch != null || mCurrentRightMatch != null) {
+ node.setAttribute(ANDROID_URI,
+ mCurrentLeftMatch != null ? ATTR_LAYOUT_TO_RIGHT_OF : ATTR_LAYOUT_TO_LEFT_OF,
+ id);
+ // Apply same top/bottom constraints as the parent
+ if (mCurrentTopMatch != null) {
+ applyConstraint(node, mCurrentTopMatch.getConstraint(true));
+ applyMargin(node, ATTR_LAYOUT_MARGIN_TOP, mTopMargin);
+ } else if (mCurrentBottomMatch != null) {
+ applyConstraint(node, mCurrentBottomMatch.getConstraint(true));
+ applyMargin(node, ATTR_LAYOUT_MARGIN_BOTTOM, mBottomMargin);
+ }
+ } else {
+ return;
+ }
+ }
+
+ /** Breaks any cycles detected by the handler */
+ public void removeCycles() {
+ if (mHorizontalCycle != null) {
+ removeCycles(mHorizontalDeps);
+ }
+ if (mVerticalCycle != null) {
+ removeCycles(mVerticalDeps);
+ }
+ }
+
+ private void removeCycles(Set<INode> deps) {
+ for (INode node : mDraggedNodes) {
+ ViewData view = mDependencyGraph.getView(node);
+ if (view != null) {
+ for (Constraint constraint : view.dependedOnBy) {
+ // For now, remove ALL constraints pointing to this node in this orientation.
+ // Later refine this to be smarter. (We can't JUST remove the constraints
+ // identified in the cycle since there could be multiple.)
+ constraint.from.node.setAttribute(ANDROID_URI, constraint.type.name, null);
+ }
+ }
+ }
+ }
+
+ /**
+ * Comparator used to sort matches such that the first match is the most desirable
+ * match (where we prefer attaching to parent bounds, we avoid matches that lead to a
+ * cycle, we prefer constraints on closer widgets rather than ones further away, and
+ * so on.)
+ * <p>
+ * There are a number of sorting criteria. One of them is the distance between the
+ * matched edges. We may end up with multiple matches that are the same distance. In
+ * that case we look at the orientation; on the left side, prefer left-oriented
+ * attachments, and on the right-side prefer right-oriented attachments. For example,
+ * consider the following scenario:
+ *
+ * <pre>
+ * +--------------------+-------------------------+
+ * | Attached on left | |
+ * +--------------------+ |
+ * | |
+ * | +-----+ |
+ * | | A | |
+ * | +-----+ |
+ * | |
+ * | +-------------------------+
+ * | | Attached on right |
+ * +--------------------+-------------------------+
+ * </pre>
+ *
+ * Here, dragging the left edge should attach to the top left attached view, whereas
+ * in the following layout dragging the right edge would attach to the bottom view:
+ *
+ * <pre>
+ * +--------------------------+-------------------+
+ * | Attached on left | |
+ * +--------------------------+ |
+ * | |
+ * | +-----+ |
+ * | | A | |
+ * | +-----+ |
+ * | |
+ * | +-------------------+
+ * | | Attached on right |
+ * +--------------------------+-------------------+
+ *
+ * </pre>
+ *
+ * </ul>
+ */
+ private final class MatchComparator implements Comparator<Match> {
+ @Override
+ public int compare(Match m1, Match m2) {
+ // Always prefer matching parent bounds
+ int parent1 = m1.edge.node == layout ? -1 : 1;
+ int parent2 = m2.edge.node == layout ? -1 : 1;
+ // unless it's a center bound -- those should always get lowest priority since
+ // they overlap with other usually more interesting edges near the center of
+ // the layout.
+ if (m1.edge.edgeType == CENTER_HORIZONTAL
+ || m1.edge.edgeType == CENTER_VERTICAL) {
+ parent1 = 2;
+ }
+ if (m2.edge.edgeType == CENTER_HORIZONTAL
+ || m2.edge.edgeType == CENTER_VERTICAL) {
+ parent2 = 2;
+ }
+ if (parent1 != parent2) {
+ return parent1 - parent2;
+ }
+
+ // Avoid matching edges that would lead to a cycle
+ if (m1.edge.edgeType.isHorizontal()) {
+ int cycle1 = mHorizontalDeps.contains(m1.edge.node) ? 1 : -1;
+ int cycle2 = mHorizontalDeps.contains(m2.edge.node) ? 1 : -1;
+ if (cycle1 != cycle2) {
+ return cycle1 - cycle2;
+ }
+ } else {
+ int cycle1 = mVerticalDeps.contains(m1.edge.node) ? 1 : -1;
+ int cycle2 = mVerticalDeps.contains(m2.edge.node) ? 1 : -1;
+ if (cycle1 != cycle2) {
+ return cycle1 - cycle2;
+ }
+ }
+
+ // TODO: Sort by minimum depth -- do we have the depth anywhere?
+
+ // Prefer nodes that are closer
+ int distance1, distance2;
+ if (m1.edge.to <= m1.with.from) {
+ distance1 = m1.with.from - m1.edge.to;
+ } else if (m1.edge.from >= m1.with.to) {
+ distance1 = m1.edge.from - m1.with.to;
+ } else {
+ // Some kind of overlap - not sure how to prioritize these yet...
+ distance1 = 0;
+ }
+ if (m2.edge.to <= m2.with.from) {
+ distance2 = m2.with.from - m2.edge.to;
+ } else if (m2.edge.from >= m2.with.to) {
+ distance2 = m2.edge.from - m2.with.to;
+ } else {
+ // Some kind of overlap - not sure how to prioritize these yet...
+ distance2 = 0;
+ }
+
+ if (distance1 != distance2) {
+ return distance1 - distance2;
+ }
+
+ // Prefer matching on baseline
+ int baseline1 = (m1.edge.edgeType == BASELINE) ? -1 : 1;
+ int baseline2 = (m2.edge.edgeType == BASELINE) ? -1 : 1;
+ if (baseline1 != baseline2) {
+ return baseline1 - baseline2;
+ }
+
+ // Prefer matching top/left edges before matching bottom/right edges
+ int orientation1 = (m1.with.edgeType == LEFT ||
+ m1.with.edgeType == TOP) ? -1 : 1;
+ int orientation2 = (m2.with.edgeType == LEFT ||
+ m2.with.edgeType == TOP) ? -1 : 1;
+ if (orientation1 != orientation2) {
+ return orientation1 - orientation2;
+ }
+
+ // Prefer opposite-matching over same-matching.
+ // In other words, if we have the choice of matching
+ // our left edge with another element's left edge,
+ // or matching our left edge with another element's right
+ // edge, prefer the right edge since that
+ // The two matches have identical distance; try to sort by
+ // orientation
+ int edgeType1 = (m1.edge.edgeType != m1.with.edgeType) ? -1 : 1;
+ int edgeType2 = (m2.edge.edgeType != m2.with.edgeType) ? -1 : 1;
+ if (edgeType1 != edgeType2) {
+ return edgeType1 - edgeType2;
+ }
+
+ return 0;
+ }
+ }
+
+ /**
+ * Returns the {@link IClientRulesEngine} IDE callback
+ *
+ * @return the {@link IClientRulesEngine} IDE callback, never null
+ */
+ public IClientRulesEngine getRulesEngine() {
+ return mRulesEngine;
+ }
+}