1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Eclipse Public License, Version 1.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.eclipse.org/org/documents/epl-v10.php 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.ide.common.layout.relative; 17 18 import static com.android.ide.common.api.MarginType.NO_MARGIN; 19 import static com.android.ide.common.api.SegmentType.BASELINE; 20 import static com.android.ide.common.api.SegmentType.BOTTOM; 21 import static com.android.ide.common.api.SegmentType.CENTER_HORIZONTAL; 22 import static com.android.ide.common.api.SegmentType.CENTER_VERTICAL; 23 import static com.android.ide.common.api.SegmentType.LEFT; 24 import static com.android.ide.common.api.SegmentType.RIGHT; 25 import static com.android.ide.common.api.SegmentType.TOP; 26 import static com.android.SdkConstants.ATTR_ID; 27 28 import static java.lang.Math.abs; 29 30 import com.android.SdkConstants; 31 import static com.android.SdkConstants.ANDROID_URI; 32 import com.android.ide.common.api.DropFeedback; 33 import com.android.ide.common.api.IClientRulesEngine; 34 import com.android.ide.common.api.IDragElement; 35 import com.android.ide.common.api.INode; 36 import com.android.ide.common.api.Rect; 37 import com.android.ide.common.api.Segment; 38 import com.android.ide.common.layout.BaseLayoutRule; 39 import com.android.ide.common.layout.relative.DependencyGraph.ViewData; 40 41 import java.util.ArrayList; 42 import java.util.List; 43 44 /** 45 * A {@link MoveHandler} is a {@link GuidelineHandler} which handles move and drop 46 * gestures, and offers guideline suggestions and snapping. 47 * <p> 48 * Unlike the {@link ResizeHandler}, the {@link MoveHandler} looks for matches for all 49 * different segment types -- the left edge, the right edge, the baseline, the center 50 * edges, and so on -- and picks the best among these. 51 */ 52 public class MoveHandler extends GuidelineHandler { 53 private int mDraggedBaseline; 54 55 /** 56 * Creates a new {@link MoveHandler}. 57 * 58 * @param layout the layout element the handler is operating on 59 * @param elements the elements being dragged in the move operation 60 * @param rulesEngine the corresponding {@link IClientRulesEngine} 61 */ MoveHandler(INode layout, IDragElement[] elements, IClientRulesEngine rulesEngine)62 public MoveHandler(INode layout, IDragElement[] elements, IClientRulesEngine rulesEngine) { 63 super(layout, rulesEngine); 64 65 // Compute list of nodes being dragged within the layout, if any 66 List<INode> nodes = new ArrayList<INode>(); 67 for (IDragElement element : elements) { 68 ViewData view = mDependencyGraph.getView(element); 69 if (view != null) { 70 nodes.add(view.node); 71 } 72 } 73 mDraggedNodes = nodes; 74 75 mHorizontalDeps = mDependencyGraph.dependsOn(nodes, false /* verticalEdge */); 76 mVerticalDeps = mDependencyGraph.dependsOn(nodes, true /* verticalEdge */); 77 78 for (INode child : layout.getChildren()) { 79 Rect bc = child.getBounds(); 80 if (bc.isValid()) { 81 // First see if this node looks like it's the same as one of the 82 // *dragged* bounds 83 boolean isDragged = false; 84 for (IDragElement element : elements) { 85 // This tries to determine if an INode corresponds to an 86 // IDragElement, by comparing their bounds. 87 if (bc.equals(element.getBounds())) { 88 isDragged = true; 89 } 90 } 91 92 if (!isDragged) { 93 String id = child.getStringAttr(ANDROID_URI, ATTR_ID); 94 // It's okay for id to be null; if you apply a constraint 95 // to a node with a missing id we will generate the id 96 97 boolean addHorizontal = !mHorizontalDeps.contains(child); 98 boolean addVertical = !mVerticalDeps.contains(child); 99 100 addBounds(child, id, addHorizontal, addVertical); 101 if (addHorizontal) { 102 addBaseLine(child, id); 103 } 104 } 105 } 106 } 107 108 String id = layout.getStringAttr(ANDROID_URI, ATTR_ID); 109 addBounds(layout, id, true, true); 110 addCenter(layout, id, true, true); 111 } 112 113 @Override snapVertical(Segment vEdge, int x, Rect newBounds)114 protected void snapVertical(Segment vEdge, int x, Rect newBounds) { 115 int maxDistance = BaseLayoutRule.getMaxMatchDistance(); 116 if (vEdge.edgeType == LEFT) { 117 int margin = !mSnap ? 0 : abs(newBounds.x - x); 118 if (margin > maxDistance) { 119 mLeftMargin = margin; 120 } else { 121 newBounds.x = x; 122 } 123 } else if (vEdge.edgeType == RIGHT) { 124 int margin = !mSnap ? 0 : abs(newBounds.x - (x - newBounds.w)); 125 if (margin > maxDistance) { 126 mRightMargin = margin; 127 } else { 128 newBounds.x = x - newBounds.w; 129 } 130 } else if (vEdge.edgeType == CENTER_VERTICAL) { 131 newBounds.x = x - newBounds.w / 2; 132 } else { 133 assert false : vEdge; 134 } 135 } 136 137 // TODO: Consider unifying this with the snapping logic in ResizeHandler 138 @Override snapHorizontal(Segment hEdge, int y, Rect newBounds)139 protected void snapHorizontal(Segment hEdge, int y, Rect newBounds) { 140 int maxDistance = BaseLayoutRule.getMaxMatchDistance(); 141 if (hEdge.edgeType == TOP) { 142 int margin = !mSnap ? 0 : abs(newBounds.y - y); 143 if (margin > maxDistance) { 144 mTopMargin = margin; 145 } else { 146 newBounds.y = y; 147 } 148 } else if (hEdge.edgeType == BOTTOM) { 149 int margin = !mSnap ? 0 : abs(newBounds.y - (y - newBounds.h)); 150 if (margin > maxDistance) { 151 mBottomMargin = margin; 152 } else { 153 newBounds.y = y - newBounds.h; 154 } 155 } else if (hEdge.edgeType == CENTER_HORIZONTAL) { 156 int margin = !mSnap ? 0 : abs(newBounds.y - (y - newBounds.h / 2)); 157 if (margin > maxDistance) { 158 mTopMargin = margin; 159 // or bottomMargin? 160 } else { 161 newBounds.y = y - newBounds.h / 2; 162 } 163 } else if (hEdge.edgeType == BASELINE) { 164 newBounds.y = y - mDraggedBaseline; 165 } else { 166 assert false : hEdge; 167 } 168 } 169 170 /** 171 * Updates the handler for the given mouse move 172 * 173 * @param feedback the feedback handler 174 * @param elements the elements being dragged 175 * @param offsetX the new mouse X coordinate 176 * @param offsetY the new mouse Y coordinate 177 * @param modifierMask the keyboard modifiers pressed during the drag 178 */ updateMove(DropFeedback feedback, IDragElement[] elements, int offsetX, int offsetY, int modifierMask)179 public void updateMove(DropFeedback feedback, IDragElement[] elements, 180 int offsetX, int offsetY, int modifierMask) { 181 mSnap = (modifierMask & DropFeedback.MODIFIER2) == 0; 182 183 Rect firstBounds = elements[0].getBounds(); 184 INode firstNode = null; 185 if (mDraggedNodes != null && mDraggedNodes.size() > 0) { 186 // TODO - this isn't quite right; this could be a different node than we have 187 // bounds for! 188 firstNode = mDraggedNodes.iterator().next(); 189 firstBounds = firstNode.getBounds(); 190 } 191 192 mBounds = new Rect(offsetX, offsetY, firstBounds.w, firstBounds.h); 193 Rect layoutBounds = layout.getBounds(); 194 if (mBounds.x2() > layoutBounds.x2()) { 195 mBounds.x -= mBounds.x2() - layoutBounds.x2(); 196 } 197 if (mBounds.y2() > layoutBounds.y2()) { 198 mBounds.y -= mBounds.y2() - layoutBounds.y2(); 199 } 200 if (mBounds.x < layoutBounds.x) { 201 mBounds.x = layoutBounds.x; 202 } 203 if (mBounds.y < layoutBounds.y) { 204 mBounds.y = layoutBounds.y; 205 } 206 207 clearSuggestions(); 208 209 Rect b = mBounds; 210 Segment edge = new Segment(b.y, b.x, b.x2(), null, null, TOP, NO_MARGIN); 211 List<Match> horizontalMatches = findClosest(edge, mHorizontalEdges); 212 edge = new Segment(b.y2(), b.x, b.x2(), null, null, BOTTOM, NO_MARGIN); 213 addClosest(edge, mHorizontalEdges, horizontalMatches); 214 215 edge = new Segment(b.x, b.y, b.y2(), null, null, LEFT, NO_MARGIN); 216 List<Match> verticalMatches = findClosest(edge, mVerticalEdges); 217 edge = new Segment(b.x2(), b.y, b.y2(), null, null, RIGHT, NO_MARGIN); 218 addClosest(edge, mVerticalEdges, verticalMatches); 219 220 // Match center 221 edge = new Segment(b.centerX(), b.y, b.y2(), null, null, CENTER_VERTICAL, NO_MARGIN); 222 addClosest(edge, mCenterVertEdges, verticalMatches); 223 edge = new Segment(b.centerY(), b.x, b.x2(), null, null, CENTER_HORIZONTAL, NO_MARGIN); 224 addClosest(edge, mCenterHorizEdges, horizontalMatches); 225 226 // Match baseline 227 if (firstNode != null) { 228 int baseline = firstNode.getBaseline(); 229 if (baseline != -1) { 230 mDraggedBaseline = baseline; 231 edge = new Segment(b.y + baseline, b.x, b.x2(), firstNode, null, BASELINE, 232 NO_MARGIN); 233 addClosest(edge, mHorizontalEdges, horizontalMatches); 234 } 235 } else { 236 int baseline = feedback.dragBaseline; 237 if (baseline != -1) { 238 mDraggedBaseline = baseline; 239 edge = new Segment(offsetY + baseline, b.x, b.x2(), null, null, BASELINE, 240 NO_MARGIN); 241 addClosest(edge, mHorizontalEdges, horizontalMatches); 242 } 243 } 244 245 mHorizontalSuggestions = horizontalMatches; 246 mVerticalSuggestions = verticalMatches; 247 mTopMargin = mBottomMargin = mLeftMargin = mRightMargin = 0; 248 249 Match match = pickBestMatch(mHorizontalSuggestions); 250 if (match != null) { 251 if (mHorizontalDeps.contains(match.edge.node)) { 252 match.cycle = true; 253 } 254 255 // Reset top AND bottom bounds regardless of whether both are bound 256 mMoveTop = true; 257 mMoveBottom = true; 258 259 // TODO: Consider doing the snap logic on all the possible matches 260 // BEFORE sorting, in case this affects the best-pick algorithm (since some 261 // edges snap and others don't). 262 snapHorizontal(match.with, match.edge.at, mBounds); 263 264 if (match.with.edgeType == TOP) { 265 mCurrentTopMatch = match; 266 } else if (match.with.edgeType == BOTTOM) { 267 mCurrentBottomMatch = match; 268 } else { 269 assert match.with.edgeType == CENTER_HORIZONTAL 270 || match.with.edgeType == BASELINE : match.with.edgeType; 271 mCurrentTopMatch = match; 272 } 273 } 274 275 match = pickBestMatch(mVerticalSuggestions); 276 if (match != null) { 277 if (mVerticalDeps.contains(match.edge.node)) { 278 match.cycle = true; 279 } 280 281 // Reset left AND right bounds regardless of whether both are bound 282 mMoveLeft = true; 283 mMoveRight = true; 284 285 snapVertical(match.with, match.edge.at, mBounds); 286 287 if (match.with.edgeType == LEFT) { 288 mCurrentLeftMatch = match; 289 } else if (match.with.edgeType == RIGHT) { 290 mCurrentRightMatch = match; 291 } else { 292 assert match.with.edgeType == CENTER_VERTICAL; 293 mCurrentLeftMatch = match; 294 } 295 } 296 297 checkCycles(feedback); 298 } 299 } 300