diff options
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.java | 839 |
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; + } +} |