/* * 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; } }