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