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