/* * 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.DrawingStyle.DEPENDENCY; import static com.android.ide.common.api.DrawingStyle.GUIDELINE; import static com.android.ide.common.api.DrawingStyle.GUIDELINE_DASHED; 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.api.SegmentType.UNKNOWN; import static com.android.ide.common.layout.relative.ConstraintType.ALIGN_BASELINE; import static com.android.ide.common.layout.relative.ConstraintType.ALIGN_BOTTOM; import static com.android.ide.common.layout.relative.ConstraintType.LAYOUT_ABOVE; import static com.android.ide.common.layout.relative.ConstraintType.LAYOUT_BELOW; import static com.android.ide.common.layout.relative.ConstraintType.LAYOUT_LEFT_OF; import static com.android.ide.common.layout.relative.ConstraintType.LAYOUT_RIGHT_OF; import com.android.ide.common.api.DrawingStyle; import com.android.ide.common.api.IGraphics; 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.SegmentType; import com.android.ide.common.layout.relative.DependencyGraph.Constraint; import com.android.ide.common.layout.relative.DependencyGraph.ViewData; import java.util.HashSet; import java.util.List; import java.util.Set; /** * The {@link ConstraintPainter} is responsible for painting relative layout constraints - * such as a source node having its top edge constrained to a target node with a given margin. * This painter is used both to show static constraints, as well as visualizing proposed * constraints during a move or resize operation. */ public class ConstraintPainter { /** The size of the arrow head */ private static final int ARROW_SIZE = 5; /** Size (height for horizontal, and width for vertical) parent feedback rectangles */ private static final int PARENT_RECT_SIZE = 12; /** * Paints a given match as a constraint. * * @param graphics the graphics context * @param sourceBounds the source bounds * @param match the match */ static void paintConstraint(IGraphics graphics, Rect sourceBounds, Match match) { Rect targetBounds = match.edge.node.getBounds(); ConstraintType type = match.type; assert type != null; paintConstraint(graphics, type, match.with.node, sourceBounds, match.edge.node, targetBounds, null /* allConstraints */, true /* highlightTargetEdge */); } /** * Paints a constraint. *
* TODO: when there are multiple links originating in the same direction from
* center, maybe offset them slightly from each other?
*
* @param graphics the graphics context to draw into
* @param constraint The constraint to be drawn
*/
private static void paintConstraint(IGraphics graphics, Constraint constraint,
Set
* Corners are a common occurrence. If we treat the horizontal and vertical
* constraints separately (below & toRightOf), then we end up with a lot of
* extra lines and arrows -- e.g. two shared edges and arrows pointing to these
* shared edges:
*
*
* Here's an example of what will be shown for a "below" constraint where the
* nodes do not overlap horizontally and the target node has a bottom margin:
*
* +--------+ |
* | Target -->
* +----|---+ |
* v
* - - - - - -|- - - - - -
* ^
* | +---|----+
* <-- Source |
* | +--------+
*
* Instead, we can simply draw a diagonal arrow here to represent BOTH constraints and
* reduce clutter:
*
* +---------+
* | Target _|
* +-------|\+
* \
* \--------+
* | Source |
* +--------+
*
*
* @param graphics the graphics context to draw
* @param type the constraint to be drawn
* @param sourceNode the source node
* @param sourceBounds the bounds of the source node
* @param targetNode the target node
* @param targetBounds the bounds of the target node
* @param allConstraints the set of all constraints; if a corner is found and painted the
* matching corner constraint is removed from the set
* @return true if the constraint was handled and painted as a corner, false otherwise
*/
private static boolean paintCornerConstraint(IGraphics graphics, ConstraintType type,
INode sourceNode, Rect sourceBounds, INode targetNode, Rect targetBounds,
Set
* +--------+
* | Target |
* +--------+
* |
* v
* - - - - - - - - - - - - - -
* ^
* |
* +--------+
* | Source |
* +--------+
*
*/
private static void paintVerticalConstraint(IGraphics graphics, ConstraintType type,
INode sourceNode, Rect sourceBounds, INode targetNode, Rect targetBounds,
boolean highlightTargetEdge) {
SegmentType sourceSegmentTypeY = type.sourceSegmentTypeY;
SegmentType targetSegmentTypeY = type.targetSegmentTypeY;
Margins targetMargins = targetNode.getMargins();
assert sourceSegmentTypeY != UNKNOWN;
assert targetBounds != null;
int sourceY = sourceSegmentTypeY.getY(sourceNode, sourceBounds);
int targetY = targetSegmentTypeY ==
UNKNOWN ? sourceY : targetSegmentTypeY.getY(targetNode, targetBounds);
if (highlightTargetEdge && type.isRelativeToParentEdge()) {
graphics.useStyle(DrawingStyle.DROP_ZONE_ACTIVE);
graphics.fillRect(targetBounds.x, targetY - PARENT_RECT_SIZE / 2,
targetBounds.x2(), targetY + PARENT_RECT_SIZE / 2);
}
// First see if the two views overlap horizontally. If so, we can just draw a direct
// arrow from the source up to (or down to) the target.
//
// +--------+
// | Target |
// +--------+
// ^
// |
// |
// +--------+
// | Source |
// +--------+
//
int maxLeft = Math.max(sourceBounds.x, targetBounds.x);
int minRight = Math.min(sourceBounds.x2(), targetBounds.x2());
int center = (maxLeft + minRight) / 2;
if (center > sourceBounds.x && center < sourceBounds.x2()) {
// Yes, the lines overlap -- just draw a straight arrow
//
//
// If however there is a margin on the target edge, it should be drawn like this:
//
// +--------+
// | Target |
// +--------+
// |
// |
// v
// - - - - - - -
// ^
// |
// |
// +--------+
// | Source |
// +--------+
//
// Use a minimum threshold for this visualization since it doesn't look good
// for small margins
if (targetSegmentTypeY == BOTTOM && targetMargins.bottom > 5) {
int sharedY = targetY + targetMargins.bottom;
if (sourceY > sharedY + 2) { // Skip when source falls on the margin line
graphics.useStyle(GUIDELINE_DASHED);
graphics.drawLine(targetBounds.x, sharedY, targetBounds.x2(), sharedY);
graphics.useStyle(GUIDELINE);
graphics.drawArrow(center, sourceY, center, sharedY + 2, ARROW_SIZE);
graphics.drawArrow(center, targetY, center, sharedY - 3, ARROW_SIZE);
} else {
graphics.useStyle(GUIDELINE);
// Draw reverse arrow to make it clear the node is as close
// at it can be
graphics.drawArrow(center, targetY, center, sourceY, ARROW_SIZE);
}
return;
} else if (targetSegmentTypeY == TOP && targetMargins.top > 5) {
int sharedY = targetY - targetMargins.top;
if (sourceY < sharedY - 2) {
graphics.useStyle(GUIDELINE_DASHED);
graphics.drawLine(targetBounds.x, sharedY, targetBounds.x2(), sharedY);
graphics.useStyle(GUIDELINE);
graphics.drawArrow(center, sourceY, center, sharedY - 3, ARROW_SIZE);
graphics.drawArrow(center, targetY, center, sharedY + 3, ARROW_SIZE);
} else {
graphics.useStyle(GUIDELINE);
graphics.drawArrow(center, targetY, center, sourceY, ARROW_SIZE);
}
return;
}
// TODO: If the center falls smack in the center of the sourceBounds,
// AND the source node is part of the selection, then adjust the
// center location such that it is off to the side, let's say 1/4 or 3/4 of
// the overlap region, to ensure that it does not overlap the center selection
// handle
// When the constraint is for two immediately adjacent edges, we
// need to make some adjustments to make sure the arrow points in the right
// direction
if (sourceY == targetY) {
if (sourceSegmentTypeY == BOTTOM || sourceSegmentTypeY == BASELINE) {
sourceY -= 2 * ARROW_SIZE;
} else if (sourceSegmentTypeY == TOP) {
sourceY += 2 * ARROW_SIZE;
} else {
assert sourceSegmentTypeY == CENTER_HORIZONTAL : sourceSegmentTypeY;
sourceY += sourceBounds.h / 2 - 2 * ARROW_SIZE;
}
} else if (sourceSegmentTypeY == BASELINE) {
sourceY = targetY - 2 * ARROW_SIZE;
}
// Center the vertical line in the overlap region
graphics.useStyle(GUIDELINE);
graphics.drawArrow(center, sourceY, center, targetY, ARROW_SIZE);
return;
}
// If there is no horizontal overlap in the vertical constraints, then we
// will show the attachment relative to a dashed line that extends beyond
// the target bounds, like this:
//
// +--------+
// | Target |
// +--------+ - - - - - - - - -
// ^
// |
// +--------+
// | Source |
// +--------+
//
// However, if the target node has a vertical margin, we may need to offset
// the line:
//
// +--------+
// | Target |
// +--------+
// |
// v
// - - - - - - - - - - - - - -
// ^
// |
// +--------+
// | Source |
// +--------+
//
// If not, we'll need to indicate a shared edge. This is the edge that separate
// them (but this will require me to evaluate margins!)
// Compute overlap region and pick the middle
int sharedY = targetSegmentTypeY ==
UNKNOWN ? sourceY : targetSegmentTypeY.getY(targetNode, targetBounds);
if (type.relativeToMargin) {
if (targetSegmentTypeY == TOP) {
sharedY -= targetMargins.top;
} else if (targetSegmentTypeY == BOTTOM) {
sharedY += targetMargins.bottom;
}
}
int startX;
int endX;
if (center <= sourceBounds.x) {
startX = targetBounds.x + targetBounds.w / 4;
endX = sourceBounds.x2();
} else {
assert (center >= sourceBounds.x2());
startX = sourceBounds.x;
endX = targetBounds.x + 3 * targetBounds.w / 4;
}
// Must draw segmented line instead
// Place the arrow 1/4 instead of 1/2 in the source to avoid overlapping with the
// selection handles
graphics.useStyle(GUIDELINE_DASHED);
graphics.drawLine(startX, sharedY, endX, sharedY);
// Adjust position of source arrow such that it does not sit across edge; it
// should point directly at the edge
if (Math.abs(sharedY - sourceY) < 2 * ARROW_SIZE) {
if (sourceSegmentTypeY == BASELINE) {
sourceY = sharedY - 2 * ARROW_SIZE;
} else if (sourceSegmentTypeY == TOP) {
sharedY = sourceY;
sourceY = sharedY + 2 * ARROW_SIZE;
} else {
sharedY = sourceY;
sourceY = sharedY - 2 * ARROW_SIZE;
}
}
graphics.useStyle(GUIDELINE);
// Draw the line from the source anchor to the shared edge
int x = sourceBounds.x + ((sourceSegmentTypeY == BASELINE) ?
sourceBounds.w / 2 : sourceBounds.w / 4);
graphics.drawArrow(x, sourceY, x, sharedY, ARROW_SIZE);
// Draw the line from the target to the horizontal shared edge
int tx = targetBounds.centerX();
if (targetSegmentTypeY == TOP) {
int ty = targetBounds.y;
int margin = targetMargins.top;
if (margin == 0 || !type.relativeToMargin) {
graphics.drawArrow(tx, ty + 2 * ARROW_SIZE, tx, ty, ARROW_SIZE);
} else {
graphics.drawArrow(tx, ty, tx, ty - margin, ARROW_SIZE);
}
} else if (targetSegmentTypeY == BOTTOM) {
int ty = targetBounds.y2();
int margin = targetMargins.bottom;
if (margin == 0 || !type.relativeToMargin) {
graphics.drawArrow(tx, ty - 2 * ARROW_SIZE, tx, ty, ARROW_SIZE);
} else {
graphics.drawArrow(tx, ty, tx, ty + margin, ARROW_SIZE);
}
} else {
assert targetSegmentTypeY == BASELINE : targetSegmentTypeY;
int ty = targetSegmentTypeY.getY(targetNode, targetBounds);
graphics.drawArrow(tx, ty - 2 * ARROW_SIZE, tx, ty, ARROW_SIZE);
}
return;
}
/**
* Paints a horizontal constraint, handling the various scenarios where there are margins,
* or where the two nodes overlap horizontally and where they don't, etc.
*/
private static void paintHorizontalConstraint(IGraphics graphics, ConstraintType type,
INode sourceNode, Rect sourceBounds, INode targetNode, Rect targetBounds,
boolean highlightTargetEdge) {
SegmentType sourceSegmentTypeX = type.sourceSegmentTypeX;
SegmentType targetSegmentTypeX = type.targetSegmentTypeX;
Margins targetMargins = targetNode.getMargins();
assert sourceSegmentTypeX != UNKNOWN;
assert targetBounds != null;
// See paintVerticalConstraint for explanations of the various cases.
int sourceX = sourceSegmentTypeX.getX(sourceNode, sourceBounds);
int targetX = targetSegmentTypeX == UNKNOWN ?
sourceX : targetSegmentTypeX.getX(targetNode, targetBounds);
if (highlightTargetEdge && type.isRelativeToParentEdge()) {
graphics.useStyle(DrawingStyle.DROP_ZONE_ACTIVE);
graphics.fillRect(targetX - PARENT_RECT_SIZE / 2, targetBounds.y,
targetX + PARENT_RECT_SIZE / 2, targetBounds.y2());
}
int maxTop = Math.max(sourceBounds.y, targetBounds.y);
int minBottom = Math.min(sourceBounds.y2(), targetBounds.y2());
// First see if the two views overlap vertically. If so, we can just draw a direct
// arrow from the source over to the target.
int center = (maxTop + minBottom) / 2;
if (center > sourceBounds.y && center < sourceBounds.y2()) {
// See if we should draw a margin line
if (targetSegmentTypeX == RIGHT && targetMargins.right > 5) {
int sharedX = targetX + targetMargins.right;
if (sourceX > sharedX + 2) { // Skip when source falls on the margin line
graphics.useStyle(GUIDELINE_DASHED);
graphics.drawLine(sharedX, targetBounds.y, sharedX, targetBounds.y2());
graphics.useStyle(GUIDELINE);
graphics.drawArrow(sourceX, center, sharedX + 2, center, ARROW_SIZE);
graphics.drawArrow(targetX, center, sharedX - 3, center, ARROW_SIZE);
} else {
graphics.useStyle(GUIDELINE);
// Draw reverse arrow to make it clear the node is as close
// at it can be
graphics.drawArrow(targetX, center, sourceX, center, ARROW_SIZE);
}
return;
} else if (targetSegmentTypeX == LEFT && targetMargins.left > 5) {
int sharedX = targetX - targetMargins.left;
if (sourceX < sharedX - 2) {
graphics.useStyle(GUIDELINE_DASHED);
graphics.drawLine(sharedX, targetBounds.y, sharedX, targetBounds.y2());
graphics.useStyle(GUIDELINE);
graphics.drawArrow(sourceX, center, sharedX - 3, center, ARROW_SIZE);
graphics.drawArrow(targetX, center, sharedX + 3, center, ARROW_SIZE);
} else {
graphics.useStyle(GUIDELINE);
graphics.drawArrow(targetX, center, sourceX, center, ARROW_SIZE);
}
return;
}
if (sourceX == targetX) {
if (sourceSegmentTypeX == RIGHT) {
sourceX -= 2 * ARROW_SIZE;
} else if (sourceSegmentTypeX == LEFT ) {
sourceX += 2 * ARROW_SIZE;
} else {
assert sourceSegmentTypeX == CENTER_VERTICAL : sourceSegmentTypeX;
sourceX += sourceBounds.w / 2 - 2 * ARROW_SIZE;
}
}
graphics.useStyle(GUIDELINE);
graphics.drawArrow(sourceX, center, targetX, center, ARROW_SIZE);
return;
}
// Segment line
// Compute overlap region and pick the middle
int sharedX = targetSegmentTypeX == UNKNOWN ?
sourceX : targetSegmentTypeX.getX(targetNode, targetBounds);
if (type.relativeToMargin) {
if (targetSegmentTypeX == LEFT) {
sharedX -= targetMargins.left;
} else if (targetSegmentTypeX == RIGHT) {
sharedX += targetMargins.right;
}
}
int startY, endY;
if (center <= sourceBounds.y) {
startY = targetBounds.y + targetBounds.h / 4;
endY = sourceBounds.y2();
} else {
assert (center >= sourceBounds.y2());
startY = sourceBounds.y;
endY = targetBounds.y + 3 * targetBounds.h / 2;
}
// Must draw segmented line instead
// Place at 1/4 instead of 1/2 to avoid overlapping with selection handles
int y = sourceBounds.y + sourceBounds.h / 4;
graphics.useStyle(GUIDELINE_DASHED);
graphics.drawLine(sharedX, startY, sharedX, endY);
// Adjust position of source arrow such that it does not sit across edge; it
// should point directly at the edge
if (Math.abs(sharedX - sourceX) < 2 * ARROW_SIZE) {
if (sourceSegmentTypeX == LEFT) {
sharedX = sourceX;
sourceX = sharedX + 2 * ARROW_SIZE;
} else {
sharedX = sourceX;
sourceX = sharedX - 2 * ARROW_SIZE;
}
}
graphics.useStyle(GUIDELINE);
// Draw the line from the source anchor to the shared edge
graphics.drawArrow(sourceX, y, sharedX, y, ARROW_SIZE);
// Draw the line from the target to the horizontal shared edge
int ty = targetBounds.centerY();
if (targetSegmentTypeX == LEFT) {
int tx = targetBounds.x;
int margin = targetMargins.left;
if (margin == 0 || !type.relativeToMargin) {
graphics.drawArrow(tx + 2 * ARROW_SIZE, ty, tx, ty, ARROW_SIZE);
} else {
graphics.drawArrow(tx, ty, tx - margin, ty, ARROW_SIZE);
}
} else {
assert targetSegmentTypeX == RIGHT;
int tx = targetBounds.x2();
int margin = targetMargins.right;
if (margin == 0 || !type.relativeToMargin) {
graphics.drawArrow(tx - 2 * ARROW_SIZE, ty, tx, ty, ARROW_SIZE);
} else {
graphics.drawArrow(tx, ty, tx + margin, ty, ARROW_SIZE);
}
}
return;
}
/**
* Paints a vertical center constraint. The constraint is shown as a dashed line
* through the vertical view, and a solid line over the node bounds.
*/
private static void paintVerticalCenterConstraint(IGraphics graphics, Rect sourceBounds,
Rect targetBounds) {
graphics.useStyle(GUIDELINE_DASHED);
graphics.drawLine(targetBounds.x, targetBounds.centerY(),
targetBounds.x2(), targetBounds.centerY());
graphics.useStyle(GUIDELINE);
graphics.drawLine(sourceBounds.x, sourceBounds.centerY(),
sourceBounds.x2(), sourceBounds.centerY());
}
/**
* Paints a horizontal center constraint. The constraint is shown as a dashed line
* through the horizontal view, and a solid line over the node bounds.
*/
private static void paintHorizontalCenterConstraint(IGraphics graphics, Rect sourceBounds,
Rect targetBounds) {
graphics.useStyle(GUIDELINE_DASHED);
graphics.drawLine(targetBounds.centerX(), targetBounds.y,
targetBounds.centerX(), targetBounds.y2());
graphics.useStyle(GUIDELINE);
graphics.drawLine(sourceBounds.centerX(), sourceBounds.y,
sourceBounds.centerX(), sourceBounds.y2());
}
}