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