blob: 0fa915d81363d6f53e7f6bb7d046c57dcf7a3ab0 [file] [log] [blame]
/*
* 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.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.SdkConstants.ATTR_ID;
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.IDragElement;
import com.android.ide.common.api.INode;
import com.android.ide.common.api.Rect;
import com.android.ide.common.api.Segment;
import com.android.ide.common.layout.BaseLayoutRule;
import com.android.ide.common.layout.relative.DependencyGraph.ViewData;
import java.util.ArrayList;
import java.util.List;
/**
* A {@link MoveHandler} is a {@link GuidelineHandler} which handles move and drop
* gestures, and offers guideline suggestions and snapping.
* <p>
* Unlike the {@link ResizeHandler}, the {@link MoveHandler} looks for matches for all
* different segment types -- the left edge, the right edge, the baseline, the center
* edges, and so on -- and picks the best among these.
*/
public class MoveHandler extends GuidelineHandler {
private int mDraggedBaseline;
/**
* Creates a new {@link MoveHandler}.
*
* @param layout the layout element the handler is operating on
* @param elements the elements being dragged in the move operation
* @param rulesEngine the corresponding {@link IClientRulesEngine}
*/
public MoveHandler(INode layout, IDragElement[] elements, IClientRulesEngine rulesEngine) {
super(layout, rulesEngine);
// Compute list of nodes being dragged within the layout, if any
List<INode> nodes = new ArrayList<INode>();
for (IDragElement element : elements) {
ViewData view = mDependencyGraph.getView(element);
if (view != null) {
nodes.add(view.node);
}
}
mDraggedNodes = nodes;
mHorizontalDeps = mDependencyGraph.dependsOn(nodes, false /* verticalEdge */);
mVerticalDeps = mDependencyGraph.dependsOn(nodes, true /* verticalEdge */);
for (INode child : layout.getChildren()) {
Rect bc = child.getBounds();
if (bc.isValid()) {
// First see if this node looks like it's the same as one of the
// *dragged* bounds
boolean isDragged = false;
for (IDragElement element : elements) {
// This tries to determine if an INode corresponds to an
// IDragElement, by comparing their bounds.
if (bc.equals(element.getBounds())) {
isDragged = true;
}
}
if (!isDragged) {
String id = child.getStringAttr(ANDROID_URI, ATTR_ID);
// It's okay for id to be null; if you apply a constraint
// to a node with a missing id we will generate the id
boolean addHorizontal = !mHorizontalDeps.contains(child);
boolean addVertical = !mVerticalDeps.contains(child);
addBounds(child, id, addHorizontal, addVertical);
if (addHorizontal) {
addBaseLine(child, id);
}
}
}
}
String id = layout.getStringAttr(ANDROID_URI, ATTR_ID);
addBounds(layout, id, true, true);
addCenter(layout, id, true, true);
}
@Override
protected void snapVertical(Segment vEdge, int x, Rect newBounds) {
int maxDistance = BaseLayoutRule.getMaxMatchDistance();
if (vEdge.edgeType == LEFT) {
int margin = !mSnap ? 0 : abs(newBounds.x - x);
if (margin > maxDistance) {
mLeftMargin = margin;
} else {
newBounds.x = x;
}
} else if (vEdge.edgeType == RIGHT) {
int margin = !mSnap ? 0 : abs(newBounds.x - (x - newBounds.w));
if (margin > maxDistance) {
mRightMargin = margin;
} else {
newBounds.x = x - newBounds.w;
}
} else if (vEdge.edgeType == CENTER_VERTICAL) {
newBounds.x = x - newBounds.w / 2;
} else {
assert false : vEdge;
}
}
// TODO: Consider unifying this with the snapping logic in ResizeHandler
@Override
protected void snapHorizontal(Segment hEdge, int y, Rect newBounds) {
int maxDistance = BaseLayoutRule.getMaxMatchDistance();
if (hEdge.edgeType == TOP) {
int margin = !mSnap ? 0 : abs(newBounds.y - y);
if (margin > maxDistance) {
mTopMargin = margin;
} else {
newBounds.y = y;
}
} else if (hEdge.edgeType == BOTTOM) {
int margin = !mSnap ? 0 : abs(newBounds.y - (y - newBounds.h));
if (margin > maxDistance) {
mBottomMargin = margin;
} else {
newBounds.y = y - newBounds.h;
}
} else if (hEdge.edgeType == CENTER_HORIZONTAL) {
int margin = !mSnap ? 0 : abs(newBounds.y - (y - newBounds.h / 2));
if (margin > maxDistance) {
mTopMargin = margin;
// or bottomMargin?
} else {
newBounds.y = y - newBounds.h / 2;
}
} else if (hEdge.edgeType == BASELINE) {
newBounds.y = y - mDraggedBaseline;
} else {
assert false : hEdge;
}
}
/**
* Updates the handler for the given mouse move
*
* @param feedback the feedback handler
* @param elements the elements being dragged
* @param offsetX the new mouse X coordinate
* @param offsetY the new mouse Y coordinate
* @param modifierMask the keyboard modifiers pressed during the drag
*/
public void updateMove(DropFeedback feedback, IDragElement[] elements,
int offsetX, int offsetY, int modifierMask) {
mSnap = (modifierMask & DropFeedback.MODIFIER2) == 0;
Rect firstBounds = elements[0].getBounds();
INode firstNode = null;
if (mDraggedNodes != null && mDraggedNodes.size() > 0) {
// TODO - this isn't quite right; this could be a different node than we have
// bounds for!
firstNode = mDraggedNodes.iterator().next();
firstBounds = firstNode.getBounds();
}
mBounds = new Rect(offsetX, offsetY, firstBounds.w, firstBounds.h);
Rect layoutBounds = layout.getBounds();
if (mBounds.x2() > layoutBounds.x2()) {
mBounds.x -= mBounds.x2() - layoutBounds.x2();
}
if (mBounds.y2() > layoutBounds.y2()) {
mBounds.y -= mBounds.y2() - layoutBounds.y2();
}
if (mBounds.x < layoutBounds.x) {
mBounds.x = layoutBounds.x;
}
if (mBounds.y < layoutBounds.y) {
mBounds.y = layoutBounds.y;
}
clearSuggestions();
Rect b = mBounds;
Segment edge = new Segment(b.y, b.x, b.x2(), null, null, TOP, NO_MARGIN);
List<Match> horizontalMatches = findClosest(edge, mHorizontalEdges);
edge = new Segment(b.y2(), b.x, b.x2(), null, null, BOTTOM, NO_MARGIN);
addClosest(edge, mHorizontalEdges, horizontalMatches);
edge = new Segment(b.x, b.y, b.y2(), null, null, LEFT, NO_MARGIN);
List<Match> verticalMatches = findClosest(edge, mVerticalEdges);
edge = new Segment(b.x2(), b.y, b.y2(), null, null, RIGHT, NO_MARGIN);
addClosest(edge, mVerticalEdges, verticalMatches);
// Match center
edge = new Segment(b.centerX(), b.y, b.y2(), null, null, CENTER_VERTICAL, NO_MARGIN);
addClosest(edge, mCenterVertEdges, verticalMatches);
edge = new Segment(b.centerY(), b.x, b.x2(), null, null, CENTER_HORIZONTAL, NO_MARGIN);
addClosest(edge, mCenterHorizEdges, horizontalMatches);
// Match baseline
if (firstNode != null) {
int baseline = firstNode.getBaseline();
if (baseline != -1) {
mDraggedBaseline = baseline;
edge = new Segment(b.y + baseline, b.x, b.x2(), firstNode, null, BASELINE,
NO_MARGIN);
addClosest(edge, mHorizontalEdges, horizontalMatches);
}
} else {
int baseline = feedback.dragBaseline;
if (baseline != -1) {
mDraggedBaseline = baseline;
edge = new Segment(offsetY + baseline, b.x, b.x2(), null, null, BASELINE,
NO_MARGIN);
addClosest(edge, mHorizontalEdges, horizontalMatches);
}
}
mHorizontalSuggestions = horizontalMatches;
mVerticalSuggestions = verticalMatches;
mTopMargin = mBottomMargin = mLeftMargin = mRightMargin = 0;
Match match = pickBestMatch(mHorizontalSuggestions);
if (match != null) {
if (mHorizontalDeps.contains(match.edge.node)) {
match.cycle = true;
}
// Reset top AND bottom bounds regardless of whether both are bound
mMoveTop = true;
mMoveBottom = true;
// TODO: Consider doing the snap logic on all the possible matches
// BEFORE sorting, in case this affects the best-pick algorithm (since some
// edges snap and others don't).
snapHorizontal(match.with, match.edge.at, mBounds);
if (match.with.edgeType == TOP) {
mCurrentTopMatch = match;
} else if (match.with.edgeType == BOTTOM) {
mCurrentBottomMatch = match;
} else {
assert match.with.edgeType == CENTER_HORIZONTAL
|| match.with.edgeType == BASELINE : match.with.edgeType;
mCurrentTopMatch = match;
}
}
match = pickBestMatch(mVerticalSuggestions);
if (match != null) {
if (mVerticalDeps.contains(match.edge.node)) {
match.cycle = true;
}
// Reset left AND right bounds regardless of whether both are bound
mMoveLeft = true;
mMoveRight = true;
snapVertical(match.with, match.edge.at, mBounds);
if (match.with.edgeType == LEFT) {
mCurrentLeftMatch = match;
} else if (match.with.edgeType == RIGHT) {
mCurrentRightMatch = match;
} else {
assert match.with.edgeType == CENTER_VERTICAL;
mCurrentLeftMatch = match;
}
}
checkCycles(feedback);
}
}