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