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.eclipse.adt.internal.editors.layout.refactoring; 17 18 import static com.android.ide.common.layout.LayoutConstants.ANDROID_URI; 19 import static com.android.ide.common.layout.LayoutConstants.ATTR_BACKGROUND; 20 import static com.android.ide.common.layout.LayoutConstants.ATTR_BASELINE_ALIGNED; 21 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ABOVE; 22 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_BASELINE; 23 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_BOTTOM; 24 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_LEFT; 25 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_BOTTOM; 26 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_LEFT; 27 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_RIGHT; 28 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_TOP; 29 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_RIGHT; 30 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_TOP; 31 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_WITH_PARENT_MISSING; 32 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_BELOW; 33 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_CENTER_HORIZONTAL; 34 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_CENTER_VERTICAL; 35 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_GRAVITY; 36 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_HEIGHT; 37 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_MARGIN_LEFT; 38 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_MARGIN_TOP; 39 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_PREFIX; 40 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_TO_LEFT_OF; 41 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_TO_RIGHT_OF; 42 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_WEIGHT; 43 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_WIDTH; 44 import static com.android.ide.common.layout.LayoutConstants.ATTR_ORIENTATION; 45 import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_BOTTOM; 46 import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_CENTER; 47 import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_CENTER_HORIZONTAL; 48 import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_CENTER_VERTICAL; 49 import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_FILL; 50 import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_FILL_HORIZONTAL; 51 import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_FILL_VERTICAL; 52 import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_LEFT; 53 import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_RIGHT; 54 import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_TOP; 55 import static com.android.ide.common.layout.LayoutConstants.ID_PREFIX; 56 import static com.android.ide.common.layout.LayoutConstants.LINEAR_LAYOUT; 57 import static com.android.ide.common.layout.LayoutConstants.NEW_ID_PREFIX; 58 import static com.android.ide.common.layout.LayoutConstants.RELATIVE_LAYOUT; 59 import static com.android.ide.common.layout.LayoutConstants.VALUE_FALSE; 60 import static com.android.ide.common.layout.LayoutConstants.VALUE_N_DP; 61 import static com.android.ide.common.layout.LayoutConstants.VALUE_TRUE; 62 import static com.android.ide.common.layout.LayoutConstants.VALUE_VERTICAL; 63 import static com.android.ide.common.layout.LayoutConstants.VALUE_WRAP_CONTENT; 64 65 import com.android.ide.eclipse.adt.AdtPlugin; 66 import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor; 67 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo; 68 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities; 69 import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; 70 import com.android.util.Pair; 71 72 import org.eclipse.core.runtime.IStatus; 73 import org.eclipse.swt.graphics.Rectangle; 74 import org.eclipse.text.edits.MultiTextEdit; 75 import org.w3c.dom.Attr; 76 import org.w3c.dom.Element; 77 import org.w3c.dom.NamedNodeMap; 78 import org.w3c.dom.Node; 79 import org.w3c.dom.NodeList; 80 81 import java.io.PrintWriter; 82 import java.io.StringWriter; 83 import java.util.ArrayList; 84 import java.util.Collections; 85 import java.util.Comparator; 86 import java.util.HashMap; 87 import java.util.HashSet; 88 import java.util.List; 89 import java.util.Map; 90 import java.util.Set; 91 92 /** 93 * Helper class which performs the bulk of the layout conversion to relative layout 94 * <p> 95 * Future enhancements: 96 * <ul> 97 * <li>Render the layout at multiple screen sizes and analyze how the widgets move and 98 * stretch and use that to add in additional constraints 99 * <li> Adapt the LinearLayout analysis code to work with TableLayouts and TableRows as well 100 * (just need to tweak the "isVertical" interpretation to account for the different defaults, 101 * and perhaps do something about column size properties. 102 * <li> We need to take into account existing margins and clear/update them 103 * </ul> 104 */ 105 class RelativeLayoutConversionHelper { 106 private final MultiTextEdit mRootEdit; 107 private final boolean mFlatten; 108 private final Element mLayout; 109 private final ChangeLayoutRefactoring mRefactoring; 110 private final CanvasViewInfo mRootView; 111 private List<Element> mDeletedElements; 112 RelativeLayoutConversionHelper(ChangeLayoutRefactoring refactoring, Element layout, boolean flatten, MultiTextEdit rootEdit, CanvasViewInfo rootView)113 RelativeLayoutConversionHelper(ChangeLayoutRefactoring refactoring, 114 Element layout, boolean flatten, MultiTextEdit rootEdit, CanvasViewInfo rootView) { 115 mRefactoring = refactoring; 116 mLayout = layout; 117 mFlatten = flatten; 118 mRootEdit = rootEdit; 119 mRootView = rootView; 120 } 121 122 /** Performs conversion from any layout to a RelativeLayout */ convertToRelative()123 public void convertToRelative() { 124 if (mRootView == null) { 125 return; 126 } 127 128 // Locate the view for the layout 129 CanvasViewInfo layoutView = findViewForElement(mRootView, mLayout); 130 if (layoutView == null || layoutView.getChildren().size() == 0) { 131 // No children. THAT was an easy conversion! 132 return; 133 } 134 135 // Study the layout and get information about how to place individual elements 136 List<View> views = analyzeLayout(layoutView); 137 138 // Create/update relative layout constraints 139 createAttachments(views); 140 } 141 142 /** Returns the elements that were deleted, or null */ getDeletedElements()143 List<Element> getDeletedElements() { 144 return mDeletedElements; 145 } 146 147 /** 148 * Analyzes the given view hierarchy and produces a list of {@link View} objects which 149 * contain placement information for each element 150 */ analyzeLayout(CanvasViewInfo layoutView)151 private List<View> analyzeLayout(CanvasViewInfo layoutView) { 152 EdgeList edgeList = new EdgeList(layoutView); 153 mDeletedElements = edgeList.getDeletedElements(); 154 deleteRemovedElements(mDeletedElements); 155 156 List<Integer> columnOffsets = edgeList.getColumnOffsets(); 157 List<Integer> rowOffsets = edgeList.getRowOffsets(); 158 159 // Compute x/y offsets for each row/column index 160 int[] left = new int[columnOffsets.size()]; 161 int[] top = new int[rowOffsets.size()]; 162 163 Map<Integer, Integer> xToCol = new HashMap<Integer, Integer>(); 164 int columnIndex = 0; 165 for (Integer offset : columnOffsets) { 166 left[columnIndex] = offset; 167 xToCol.put(offset, columnIndex++); 168 } 169 Map<Integer, Integer> yToRow = new HashMap<Integer, Integer>(); 170 int rowIndex = 0; 171 for (Integer offset : rowOffsets) { 172 top[rowIndex] = offset; 173 yToRow.put(offset, rowIndex++); 174 } 175 176 // Create a complete list of view objects 177 List<View> views = createViews(edgeList, columnOffsets); 178 initializeSpans(edgeList, columnOffsets, rowOffsets, xToCol, yToRow); 179 180 // Sanity check 181 for (View view : views) { 182 assert view.getLeftEdge() == left[view.mCol]; 183 assert view.getTopEdge() == top[view.mRow]; 184 assert view.getRightEdge() == left[view.mCol+view.mColSpan]; 185 assert view.getBottomEdge() == top[view.mRow+view.mRowSpan]; 186 } 187 188 // Ensure that every view has a proper id such that it can be referred to 189 // with a constraint 190 initializeIds(edgeList, views); 191 192 // Attempt to lay the views out in a grid with constraints (though not that widgets 193 // can overlap as well) 194 Grid grid = new Grid(views, left, top); 195 computeKnownConstraints(views, edgeList); 196 computeHorizontalConstraints(grid); 197 computeVerticalConstraints(grid); 198 199 return views; 200 } 201 202 /** Produces a list of {@link View} objects from an {@link EdgeList} */ createViews(EdgeList edgeList, List<Integer> columnOffsets)203 private List<View> createViews(EdgeList edgeList, List<Integer> columnOffsets) { 204 List<View> views = new ArrayList<View>(); 205 for (Integer offset : columnOffsets) { 206 List<View> leftEdgeViews = edgeList.getLeftEdgeViews(offset); 207 if (leftEdgeViews == null) { 208 // must have been a right edge 209 continue; 210 } 211 for (View view : leftEdgeViews) { 212 views.add(view); 213 } 214 } 215 return views; 216 } 217 218 /** Removes any elements targeted for deletion */ deleteRemovedElements(List<Element> delete)219 private void deleteRemovedElements(List<Element> delete) { 220 if (mFlatten && delete.size() > 0) { 221 for (Element element : delete) { 222 mRefactoring.removeElementTags(mRootEdit, element, delete, 223 !AdtPrefs.getPrefs().getFormatGuiXml() /*changeIndentation*/); 224 } 225 } 226 } 227 228 /** Ensures that every element has an id such that it can be referenced from a constraint */ initializeIds(EdgeList edgeList, List<View> views)229 private void initializeIds(EdgeList edgeList, List<View> views) { 230 // Ensure that all views have a valid id 231 for (View view : views) { 232 String id = mRefactoring.ensureHasId(mRootEdit, view.mElement, null); 233 edgeList.setIdAttributeValue(view, id); 234 } 235 } 236 237 /** 238 * Initializes the column and row indices, as well as any column span and row span 239 * values 240 */ initializeSpans(EdgeList edgeList, List<Integer> columnOffsets, List<Integer> rowOffsets, Map<Integer, Integer> xToCol, Map<Integer, Integer> yToRow)241 private void initializeSpans(EdgeList edgeList, List<Integer> columnOffsets, 242 List<Integer> rowOffsets, Map<Integer, Integer> xToCol, Map<Integer, Integer> yToRow) { 243 // Now initialize table view row, column and spans 244 for (Integer offset : columnOffsets) { 245 List<View> leftEdgeViews = edgeList.getLeftEdgeViews(offset); 246 if (leftEdgeViews == null) { 247 // must have been a right edge 248 continue; 249 } 250 for (View view : leftEdgeViews) { 251 Integer col = xToCol.get(view.getLeftEdge()); 252 assert col != null; 253 Integer end = xToCol.get(view.getRightEdge()); 254 assert end != null; 255 256 view.mCol = col; 257 view.mColSpan = end - col; 258 } 259 } 260 261 for (Integer offset : rowOffsets) { 262 List<View> topEdgeViews = edgeList.getTopEdgeViews(offset); 263 if (topEdgeViews == null) { 264 // must have been a bottom edge 265 continue; 266 } 267 for (View view : topEdgeViews) { 268 Integer row = yToRow.get(view.getTopEdge()); 269 assert row != null; 270 Integer end = yToRow.get(view.getBottomEdge()); 271 assert end != null; 272 273 view.mRow = row; 274 view.mRowSpan = end - row; 275 } 276 } 277 } 278 279 /** 280 * Creates refactoring edits which adds or updates constraints for the given list of 281 * views 282 */ createAttachments(List<View> views)283 private void createAttachments(List<View> views) { 284 // Make the attachments 285 String namespace = mRefactoring.getAndroidNamespacePrefix(); 286 for (View view : views) { 287 for (Pair<String, String> constraint : view.getHorizConstraints()) { 288 mRefactoring.setAttribute(mRootEdit, view.mElement, ANDROID_URI, 289 namespace, constraint.getFirst(), constraint.getSecond()); 290 } 291 for (Pair<String, String> constraint : view.getVerticalConstraints()) { 292 mRefactoring.setAttribute(mRootEdit, view.mElement, ANDROID_URI, 293 namespace, constraint.getFirst(), constraint.getSecond()); 294 } 295 } 296 } 297 298 /** 299 * Analyzes the existing layouts and layout parameter objects in the document to infer 300 * constraints for layout types that we know about - such as LinearLayout baseline 301 * alignment, weights, gravity, etc. 302 */ computeKnownConstraints(List<View> views, EdgeList edgeList)303 private void computeKnownConstraints(List<View> views, EdgeList edgeList) { 304 // List of parent layout elements we've already processed. We iterate through all 305 // the -children-, and we ask each for its element parent (which won't have a view) 306 // and we look at the parent's layout attributes and its children layout constraints, 307 // and then we stash away constraints that we can infer. This means that we will 308 // encounter the same parent for every sibling, so that's why there's a map to 309 // prevent duplicate work. 310 Set<Node> seen = new HashSet<Node>(); 311 312 for (View view : views) { 313 Element element = view.getElement(); 314 Node parent = element.getParentNode(); 315 if (seen.contains(parent)) { 316 continue; 317 } 318 seen.add(parent); 319 320 if (parent.getNodeType() != Node.ELEMENT_NODE) { 321 continue; 322 } 323 Element layout = (Element) parent; 324 String layoutName = layout.getTagName(); 325 326 if (LINEAR_LAYOUT.equals(layoutName)) { 327 analyzeLinearLayout(edgeList, layout); 328 } else if (RELATIVE_LAYOUT.equals(layoutName)) { 329 analyzeRelativeLayout(edgeList, layout); 330 } else { 331 // Some other layout -- add more conditional handling here 332 // for framelayout, tables, etc. 333 } 334 } 335 } 336 337 public static final int GRAVITY_LEFT = 1 << 0; 338 public static final int GRAVITY_RIGHT = 1<< 1; 339 public static final int GRAVITY_CENTER_HORIZ = 1 << 2; 340 public static final int GRAVITY_FILL_HORIZ = 1 << 3; 341 public static final int GRAVITY_CENTER_VERT = 1 << 4; 342 public static final int GRAVITY_FILL_VERT = 1 << 5; 343 public static final int GRAVITY_TOP = 1 << 6; 344 public static final int GRAVITY_BOTTOM = 1 << 7; 345 public static final int GRAVITY_HORIZ_MASK = GRAVITY_CENTER_HORIZ | GRAVITY_FILL_HORIZ 346 | GRAVITY_LEFT | GRAVITY_RIGHT; 347 public static final int GRAVITY_VERT_MASK = GRAVITY_CENTER_VERT | GRAVITY_FILL_VERT 348 | GRAVITY_TOP | GRAVITY_BOTTOM; 349 350 /** 351 * Returns the gravity of the given element 352 * 353 * @param element the element to look up the gravity for 354 * @return a bit mask corresponding to the selected gravities 355 */ getGravity(Element element)356 public static int getGravity(Element element) { 357 String gravityString = element.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_GRAVITY); 358 return getGravity(gravityString, GRAVITY_LEFT | GRAVITY_TOP); 359 } 360 361 /** 362 * Returns the gravity bitmask for the given gravity string description 363 * 364 * @param gravityString the gravity string description 365 * @param defaultMask the default/initial bitmask to start with 366 * @return a bitmask corresponding to the gravity description 367 */ getGravity(String gravityString, int defaultMask)368 public static int getGravity(String gravityString, int defaultMask) { 369 int gravity = defaultMask; 370 if (gravityString != null && gravityString.length() > 0) { 371 String[] anchors = gravityString.split("\\|"); //$NON-NLS-1$ 372 for (String anchor : anchors) { 373 if (GRAVITY_VALUE_CENTER.equals(anchor)) { 374 gravity = GRAVITY_CENTER_HORIZ | GRAVITY_CENTER_VERT; 375 } else if (GRAVITY_VALUE_FILL.equals(anchor)) { 376 gravity = GRAVITY_FILL_HORIZ | GRAVITY_FILL_VERT; 377 } else if (GRAVITY_VALUE_CENTER_VERTICAL.equals(anchor)) { 378 gravity = (gravity & GRAVITY_HORIZ_MASK) | GRAVITY_CENTER_VERT; 379 } else if (GRAVITY_VALUE_CENTER_HORIZONTAL.equals(anchor)) { 380 gravity = (gravity & GRAVITY_VERT_MASK) | GRAVITY_CENTER_HORIZ; 381 } else if (GRAVITY_VALUE_FILL_VERTICAL.equals(anchor)) { 382 gravity = (gravity & GRAVITY_HORIZ_MASK) | GRAVITY_FILL_VERT; 383 } else if (GRAVITY_VALUE_FILL_HORIZONTAL.equals(anchor)) { 384 gravity = (gravity & GRAVITY_VERT_MASK) | GRAVITY_FILL_HORIZ; 385 } else if (GRAVITY_VALUE_TOP.equals(anchor)) { 386 gravity = (gravity & GRAVITY_HORIZ_MASK) | GRAVITY_TOP; 387 } else if (GRAVITY_VALUE_BOTTOM.equals(anchor)) { 388 gravity = (gravity & GRAVITY_HORIZ_MASK) | GRAVITY_BOTTOM; 389 } else if (GRAVITY_VALUE_LEFT.equals(anchor)) { 390 gravity = (gravity & GRAVITY_VERT_MASK) | GRAVITY_LEFT; 391 } else if (GRAVITY_VALUE_RIGHT.equals(anchor)) { 392 gravity = (gravity & GRAVITY_VERT_MASK) | GRAVITY_RIGHT; 393 } else { 394 // "clip" not supported 395 } 396 } 397 } 398 399 return gravity; 400 } 401 402 /** 403 * Returns the layout weight of of the given child of a LinearLayout, or 0.0 if it 404 * does not define a weight 405 */ getWeight(Element linearLayoutChild)406 private float getWeight(Element linearLayoutChild) { 407 String weight = linearLayoutChild.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_WEIGHT); 408 if (weight != null && weight.length() > 0) { 409 try { 410 return Float.parseFloat(weight); 411 } catch (NumberFormatException nfe) { 412 AdtPlugin.log(nfe, "Invalid weight %1$s", weight); 413 } 414 } 415 416 return 0.0f; 417 } 418 419 /** 420 * Returns the sum of all the layout weights of the children in the given LinearLayout 421 * 422 * @param linearLayout the layout to compute the total sum for 423 * @return the total sum of all the layout weights in the given layout 424 */ getWeightSum(Element linearLayout)425 private float getWeightSum(Element linearLayout) { 426 float sum = 0; 427 for (Element child : DomUtilities.getChildren(linearLayout)) { 428 sum += getWeight(child); 429 } 430 431 return sum; 432 } 433 434 /** 435 * Analyzes the given LinearLayout and updates the constraints to reflect 436 * relationships it can infer - based on baseline alignment, gravity, order and 437 * weights. This method also removes "0dip" as a special width/height used in 438 * LinearLayouts with weight distribution. 439 */ analyzeLinearLayout(EdgeList edgeList, Element layout)440 private void analyzeLinearLayout(EdgeList edgeList, Element layout) { 441 boolean isVertical = VALUE_VERTICAL.equals(layout.getAttributeNS(ANDROID_URI, 442 ATTR_ORIENTATION)); 443 View baselineRef = null; 444 if (!isVertical && 445 !VALUE_FALSE.equals(layout.getAttributeNS(ANDROID_URI, ATTR_BASELINE_ALIGNED))) { 446 // Baseline alignment. Find the tallest child and set it as the baseline reference. 447 int tallestHeight = 0; 448 View tallest = null; 449 for (Element child : DomUtilities.getChildren(layout)) { 450 View view = edgeList.getView(child); 451 if (view != null && view.getHeight() > tallestHeight) { 452 tallestHeight = view.getHeight(); 453 tallest = view; 454 } 455 } 456 if (tallest != null) { 457 baselineRef = tallest; 458 } 459 } 460 461 float weightSum = getWeightSum(layout); 462 float cumulativeWeight = 0; 463 464 List<Element> children = DomUtilities.getChildren(layout); 465 String prevId = null; 466 boolean isFirstChild = true; 467 boolean linkBackwards = true; 468 boolean linkForwards = false; 469 470 for (int index = 0, childCount = children.size(); index < childCount; index++) { 471 Element child = children.get(index); 472 473 View childView = edgeList.getView(child); 474 if (childView == null) { 475 // Could be a nested layout that is being removed etc 476 prevId = null; 477 isFirstChild = false; 478 continue; 479 } 480 481 // Look at the layout_weight attributes and determine whether we should be 482 // attached on the bottom/right or on the top/left 483 if (weightSum > 0.0f) { 484 float weight = getWeight(child); 485 486 // We can't emulate a LinearLayout where multiple children have positive 487 // weights. However, we CAN support the common scenario where a single 488 // child has a non-zero weight, and all children after it are pushed 489 // to the end and the weighted child fills the remaining space. 490 if (cumulativeWeight == 0 && weight > 0) { 491 // See if we have a bottom/right edge to attach the forwards link to 492 // (at the end of the forwards chains). Only if so can we link forwards. 493 View referenced; 494 if (isVertical) { 495 referenced = edgeList.getSharedBottomEdge(layout); 496 } else { 497 referenced = edgeList.getSharedRightEdge(layout); 498 } 499 if (referenced != null) { 500 linkForwards = true; 501 } 502 } else if (cumulativeWeight > 0) { 503 linkBackwards = false; 504 } 505 506 cumulativeWeight += weight; 507 } 508 509 analyzeGravity(edgeList, layout, isVertical, child, childView); 510 convert0dipToWrapContent(child); 511 512 // Chain elements together in the flow direction of the linear layout 513 if (prevId != null) { // No constraint for first child 514 if (linkBackwards) { 515 if (isVertical) { 516 childView.addVerticalConstraint(ATTR_LAYOUT_BELOW, prevId); 517 } else { 518 childView.addHorizConstraint(ATTR_LAYOUT_TO_RIGHT_OF, prevId); 519 } 520 } 521 } else if (isFirstChild) { 522 assert linkBackwards; 523 524 // First element; attach it to the parent if we can 525 if (isVertical) { 526 View referenced = edgeList.getSharedTopEdge(layout); 527 if (referenced != null) { 528 if (isAncestor(referenced.getElement(), child)) { 529 childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_PARENT_TOP, 530 VALUE_TRUE); 531 } else { 532 childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_TOP, 533 referenced.getId()); 534 } 535 } 536 } else { 537 View referenced = edgeList.getSharedLeftEdge(layout); 538 if (referenced != null) { 539 if (isAncestor(referenced.getElement(), child)) { 540 childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_PARENT_LEFT, 541 VALUE_TRUE); 542 } else { 543 childView.addHorizConstraint( 544 ATTR_LAYOUT_ALIGN_LEFT, referenced.getId()); 545 } 546 } 547 } 548 } 549 550 if (linkForwards) { 551 if (index < (childCount - 1)) { 552 Element nextChild = children.get(index + 1); 553 String nextId = mRefactoring.ensureHasId(mRootEdit, nextChild, null); 554 if (nextId != null) { 555 if (isVertical) { 556 childView.addVerticalConstraint(ATTR_LAYOUT_ABOVE, nextId); 557 } else { 558 childView.addHorizConstraint(ATTR_LAYOUT_TO_LEFT_OF, nextId); 559 } 560 } 561 } else { 562 // Attach to right/bottom edge of the layout 563 if (isVertical) { 564 View referenced = edgeList.getSharedBottomEdge(layout); 565 if (referenced != null) { 566 if (isAncestor(referenced.getElement(), child)) { 567 childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_PARENT_BOTTOM, 568 VALUE_TRUE); 569 } else { 570 childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_BOTTOM, 571 referenced.getId()); 572 } 573 } 574 } else { 575 View referenced = edgeList.getSharedRightEdge(layout); 576 if (referenced != null) { 577 if (isAncestor(referenced.getElement(), child)) { 578 childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_PARENT_RIGHT, 579 VALUE_TRUE); 580 } else { 581 childView.addHorizConstraint( 582 ATTR_LAYOUT_ALIGN_RIGHT, referenced.getId()); 583 } 584 } 585 } 586 } 587 } 588 589 if (baselineRef != null && baselineRef.getId() != null 590 && !baselineRef.getId().equals(childView.getId())) { 591 assert !isVertical; 592 // Only align if they share the same gravity 593 if ((childView.getGravity() & GRAVITY_VERT_MASK) == 594 (baselineRef.getGravity() & GRAVITY_VERT_MASK)) { 595 childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_BASELINE, baselineRef.getId()); 596 } 597 } 598 599 prevId = mRefactoring.ensureHasId(mRootEdit, child, null); 600 isFirstChild = false; 601 } 602 } 603 604 /** 605 * Checks the layout "gravity" value for the given child and updates the constraints 606 * to account for the gravity 607 */ analyzeGravity(EdgeList edgeList, Element layout, boolean isVertical, Element child, View childView)608 private int analyzeGravity(EdgeList edgeList, Element layout, boolean isVertical, 609 Element child, View childView) { 610 // Use gravity to constrain elements in the axis orthogonal to the 611 // direction of the layout 612 int gravity = childView.getGravity(); 613 if (isVertical) { 614 if ((gravity & GRAVITY_RIGHT) != 0) { 615 View referenced = edgeList.getSharedRightEdge(layout); 616 if (referenced != null) { 617 if (isAncestor(referenced.getElement(), child)) { 618 childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_PARENT_RIGHT, 619 VALUE_TRUE); 620 } else { 621 childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_RIGHT, 622 referenced.getId()); 623 } 624 } 625 } else if ((gravity & GRAVITY_CENTER_HORIZ) != 0) { 626 View referenced1 = edgeList.getSharedLeftEdge(layout); 627 View referenced2 = edgeList.getSharedRightEdge(layout); 628 if (referenced1 != null && referenced2 == referenced1) { 629 if (isAncestor(referenced1.getElement(), child)) { 630 childView.addHorizConstraint(ATTR_LAYOUT_CENTER_HORIZONTAL, 631 VALUE_TRUE); 632 } 633 } 634 } else if ((gravity & GRAVITY_FILL_HORIZ) != 0) { 635 View referenced1 = edgeList.getSharedLeftEdge(layout); 636 View referenced2 = edgeList.getSharedRightEdge(layout); 637 if (referenced1 != null && referenced2 == referenced1) { 638 if (isAncestor(referenced1.getElement(), child)) { 639 childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_PARENT_LEFT, 640 VALUE_TRUE); 641 childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_PARENT_RIGHT, 642 VALUE_TRUE); 643 } else { 644 childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_LEFT, 645 referenced1.getId()); 646 childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_RIGHT, 647 referenced2.getId()); 648 } 649 } 650 } else if ((gravity & GRAVITY_LEFT) != 0) { 651 View referenced = edgeList.getSharedLeftEdge(layout); 652 if (referenced != null) { 653 if (isAncestor(referenced.getElement(), child)) { 654 childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_PARENT_LEFT, 655 VALUE_TRUE); 656 } else { 657 childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_LEFT, 658 referenced.getId()); 659 } 660 } 661 } 662 } else { 663 // Handle horizontal layout: perform vertical gravity attachments 664 if ((gravity & GRAVITY_BOTTOM) != 0) { 665 View referenced = edgeList.getSharedBottomEdge(layout); 666 if (referenced != null) { 667 if (isAncestor(referenced.getElement(), child)) { 668 childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_PARENT_BOTTOM, 669 VALUE_TRUE); 670 } else { 671 childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_BOTTOM, 672 referenced.getId()); 673 } 674 } 675 } else if ((gravity & GRAVITY_CENTER_VERT) != 0) { 676 View referenced1 = edgeList.getSharedTopEdge(layout); 677 View referenced2 = edgeList.getSharedBottomEdge(layout); 678 if (referenced1 != null && referenced2 == referenced1) { 679 if (isAncestor(referenced1.getElement(), child)) { 680 childView.addVerticalConstraint(ATTR_LAYOUT_CENTER_VERTICAL, 681 VALUE_TRUE); 682 } 683 } 684 } else if ((gravity & GRAVITY_FILL_VERT) != 0) { 685 View referenced1 = edgeList.getSharedTopEdge(layout); 686 View referenced2 = edgeList.getSharedBottomEdge(layout); 687 if (referenced1 != null && referenced2 == referenced1) { 688 if (isAncestor(referenced1.getElement(), child)) { 689 childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_PARENT_TOP, 690 VALUE_TRUE); 691 childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_PARENT_BOTTOM, 692 VALUE_TRUE); 693 } else { 694 childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_TOP, 695 referenced1.getId()); 696 childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_BOTTOM, 697 referenced2.getId()); 698 } 699 } 700 } else if ((gravity & GRAVITY_TOP) != 0) { 701 View referenced = edgeList.getSharedTopEdge(layout); 702 if (referenced != null) { 703 if (isAncestor(referenced.getElement(), child)) { 704 childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_PARENT_TOP, 705 VALUE_TRUE); 706 } else { 707 childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_TOP, 708 referenced.getId()); 709 } 710 } 711 } 712 } 713 return gravity; 714 } 715 716 /** Converts 0dip values in layout_width and layout_height to wrap_content instead */ convert0dipToWrapContent(Element child)717 private void convert0dipToWrapContent(Element child) { 718 // Must convert layout_height="0dip" to layout_height="wrap_content". 719 // 0dip is a special trick used in linear layouts in the presence of 720 // weights where 0dip ensures that the height of the view is not taken 721 // into account when distributing the weights. However, when converted 722 // to RelativeLayout this will instead cause the view to actually be assigned 723 // 0 height. 724 String height = child.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT); 725 // 0dip, 0dp, 0px, etc 726 if (height != null && height.startsWith("0")) { //$NON-NLS-1$ 727 mRefactoring.setAttribute(mRootEdit, child, ANDROID_URI, 728 mRefactoring.getAndroidNamespacePrefix(), ATTR_LAYOUT_HEIGHT, 729 VALUE_WRAP_CONTENT); 730 } 731 String width = child.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH); 732 if (width != null && width.startsWith("0")) { //$NON-NLS-1$ 733 mRefactoring.setAttribute(mRootEdit, child, ANDROID_URI, 734 mRefactoring.getAndroidNamespacePrefix(), ATTR_LAYOUT_WIDTH, 735 VALUE_WRAP_CONTENT); 736 } 737 } 738 739 /** 740 * Analyzes an embedded RelativeLayout within a layout hierarchy and updates the 741 * constraints in the EdgeList with those relationships which can continue in the 742 * outer single RelativeLayout. 743 */ analyzeRelativeLayout(EdgeList edgeList, Element layout)744 private void analyzeRelativeLayout(EdgeList edgeList, Element layout) { 745 NodeList children = layout.getChildNodes(); 746 for (int i = 0, n = children.getLength(); i < n; i++) { 747 Node node = children.item(i); 748 if (node.getNodeType() == Node.ELEMENT_NODE) { 749 Element child = (Element) node; 750 View childView = edgeList.getView(child); 751 if (childView == null) { 752 // Could be a nested layout that is being removed etc 753 continue; 754 } 755 756 NamedNodeMap attributes = child.getAttributes(); 757 for (int j = 0, m = attributes.getLength(); j < m; j++) { 758 Attr attribute = (Attr) attributes.item(j); 759 String name = attribute.getLocalName(); 760 String value = attribute.getValue(); 761 if (name.equals(ATTR_LAYOUT_WIDTH) 762 || name.equals(ATTR_LAYOUT_HEIGHT)) { 763 // Ignore these for now 764 } else if (name.startsWith(ATTR_LAYOUT_PREFIX) 765 && ANDROID_URI.equals(attribute.getNamespaceURI())) { 766 // Determine if the reference is to a known edge 767 String id = getIdBasename(value); 768 if (id != null) { 769 View referenced = edgeList.getView(id); 770 if (referenced != null) { 771 // This is a valid reference, so preserve 772 // the attribute 773 if (name.equals(ATTR_LAYOUT_BELOW) || 774 name.equals(ATTR_LAYOUT_ABOVE) || 775 name.equals(ATTR_LAYOUT_ALIGN_TOP) || 776 name.equals(ATTR_LAYOUT_ALIGN_BOTTOM) || 777 name.equals(ATTR_LAYOUT_ALIGN_BASELINE)) { 778 // Vertical constraint 779 childView.addVerticalConstraint(name, value); 780 } else if (name.equals(ATTR_LAYOUT_ALIGN_LEFT) || 781 name.equals(ATTR_LAYOUT_TO_LEFT_OF) || 782 name.equals(ATTR_LAYOUT_TO_RIGHT_OF) || 783 name.equals(ATTR_LAYOUT_ALIGN_RIGHT)) { 784 // Horizontal constraint 785 childView.addHorizConstraint(name, value); 786 } else { 787 // We don't expect this 788 assert false : name; 789 } 790 } else { 791 // Reference to some layout that is not included here. 792 // TODO: See if the given layout has an edge 793 // that corresponds to one of our known views 794 // so we can adjust the constraints and keep it after all. 795 } 796 } else { 797 // It's a parent-relative constraint (such 798 // as aligning with a parent edge, or centering 799 // in the parent view) 800 boolean remove = true; 801 if (name.equals(ATTR_LAYOUT_ALIGN_PARENT_LEFT)) { 802 View referenced = edgeList.getSharedLeftEdge(layout); 803 if (referenced != null) { 804 if (isAncestor(referenced.getElement(), child)) { 805 childView.addHorizConstraint(name, VALUE_TRUE); 806 } else { 807 childView.addHorizConstraint( 808 ATTR_LAYOUT_ALIGN_LEFT, referenced.getId()); 809 } 810 remove = false; 811 } 812 } else if (name.equals(ATTR_LAYOUT_ALIGN_PARENT_RIGHT)) { 813 View referenced = edgeList.getSharedRightEdge(layout); 814 if (referenced != null) { 815 if (isAncestor(referenced.getElement(), child)) { 816 childView.addHorizConstraint(name, VALUE_TRUE); 817 } else { 818 childView.addHorizConstraint( 819 ATTR_LAYOUT_ALIGN_RIGHT, referenced.getId()); 820 } 821 remove = false; 822 } 823 } else if (name.equals(ATTR_LAYOUT_ALIGN_PARENT_TOP)) { 824 View referenced = edgeList.getSharedTopEdge(layout); 825 if (referenced != null) { 826 if (isAncestor(referenced.getElement(), child)) { 827 childView.addVerticalConstraint(name, VALUE_TRUE); 828 } else { 829 childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_TOP, 830 referenced.getId()); 831 } 832 remove = false; 833 } 834 } else if (name.equals(ATTR_LAYOUT_ALIGN_PARENT_BOTTOM)) { 835 View referenced = edgeList.getSharedBottomEdge(layout); 836 if (referenced != null) { 837 if (isAncestor(referenced.getElement(), child)) { 838 childView.addVerticalConstraint(name, VALUE_TRUE); 839 } else { 840 childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_BOTTOM, 841 referenced.getId()); 842 } 843 remove = false; 844 } 845 } 846 847 boolean alignWithParent = 848 name.equals(ATTR_LAYOUT_ALIGN_WITH_PARENT_MISSING); 849 if (remove && alignWithParent) { 850 // TODO - look for this one AFTER we have processed 851 // everything else, and then set constraints as necessary 852 // IF there are no other conflicting constraints! 853 } 854 855 // Otherwise it's some kind of centering which we don't support 856 // yet. 857 858 // TODO: Find a way to determine whether we have 859 // a corresponding edge for the parent (e.g. if 860 // the ViewInfo bounds match our outer parent or 861 // some other edge) and if so, substitute for that 862 // id. 863 // For example, if this element was centered 864 // horizontally in a RelativeLayout that actually 865 // occupies the entire width of our outer layout, 866 // then it can be preserved after all! 867 868 if (remove) { 869 if (name.startsWith("layout_margin")) { //$NON-NLS-1$ 870 continue; 871 } 872 873 // Remove unknown attributes? 874 // It's too early to do this, because we may later want 875 // to *set* this value and it would result in an overlapping edits 876 // exception. Therefore, we need to RECORD which attributes should 877 // be removed, which lines should have its indentation adjusted 878 // etc and finally process it all at the end! 879 //mRefactoring.removeAttribute(mRootEdit, child, 880 // attribute.getNamespaceURI(), name); 881 } 882 } 883 } 884 } 885 } 886 } 887 } 888 889 /** 890 * Given {@code @id/foo} or {@code @+id/foo}, returns foo. Note that given foo it will 891 * return null. 892 */ getIdBasename(String id)893 private static String getIdBasename(String id) { 894 if (id.startsWith(NEW_ID_PREFIX)) { 895 return id.substring(NEW_ID_PREFIX.length()); 896 } else if (id.startsWith(ID_PREFIX)) { 897 return id.substring(ID_PREFIX.length()); 898 } 899 900 return null; 901 } 902 903 /** Returns true if the given second argument is a descendant of the first argument */ isAncestor(Node ancestor, Node node)904 private static boolean isAncestor(Node ancestor, Node node) { 905 while (node != null) { 906 if (node == ancestor) { 907 return true; 908 } 909 node = node.getParentNode(); 910 } 911 return false; 912 } 913 914 /** 915 * Computes horizontal constraints for the views in the grid for any remaining views 916 * that do not have constraints (as the result of the analysis of known layouts). This 917 * will look at the rendered layout coordinates and attempt to connect elements based 918 * on a spatial layout in the grid. 919 */ computeHorizontalConstraints(Grid grid)920 private void computeHorizontalConstraints(Grid grid) { 921 int columns = grid.getColumns(); 922 923 String attachLeftProperty = ATTR_LAYOUT_ALIGN_PARENT_LEFT; 924 String attachLeftValue = VALUE_TRUE; 925 int marginLeft = 0; 926 for (int col = 0; col < columns; col++) { 927 if (!grid.colContainsTopLeftCorner(col)) { 928 // Just accumulate margins for the next column 929 marginLeft += grid.getColumnWidth(col); 930 } else { 931 // Add horizontal attachments 932 String firstId = null; 933 for (View view : grid.viewsStartingInCol(col, true)) { 934 assert view.getId() != null; 935 if (firstId == null) { 936 firstId = view.getId(); 937 if (view.isConstrainedHorizontally()) { 938 // Nothing to do -- we already have an accurate position for 939 // this view 940 } else if (attachLeftProperty != null) { 941 view.addHorizConstraint(attachLeftProperty, attachLeftValue); 942 if (marginLeft > 0) { 943 view.addHorizConstraint(ATTR_LAYOUT_MARGIN_LEFT, 944 String.format(VALUE_N_DP, marginLeft)); 945 marginLeft = 0; 946 } 947 } else { 948 assert false; 949 } 950 } else if (!view.isConstrainedHorizontally()) { 951 view.addHorizConstraint(ATTR_LAYOUT_ALIGN_LEFT, firstId); 952 } 953 } 954 } 955 956 // Figure out edge for the next column 957 View view = grid.findRightEdgeView(col); 958 if (view != null) { 959 assert view.getId() != null; 960 attachLeftProperty = ATTR_LAYOUT_TO_RIGHT_OF; 961 attachLeftValue = view.getId(); 962 963 marginLeft = 0; 964 } else if (marginLeft == 0) { 965 marginLeft = grid.getColumnWidth(col); 966 } 967 } 968 } 969 970 /** 971 * Performs vertical layout just like the {@link #computeHorizontalConstraints} method 972 * did horizontally 973 */ computeVerticalConstraints(Grid grid)974 private void computeVerticalConstraints(Grid grid) { 975 int rows = grid.getRows(); 976 977 String attachTopProperty = ATTR_LAYOUT_ALIGN_PARENT_TOP; 978 String attachTopValue = VALUE_TRUE; 979 int marginTop = 0; 980 for (int row = 0; row < rows; row++) { 981 if (!grid.rowContainsTopLeftCorner(row)) { 982 // Just accumulate margins for the next column 983 marginTop += grid.getRowHeight(row); 984 } else { 985 // Add horizontal attachments 986 String firstId = null; 987 for (View view : grid.viewsStartingInRow(row, true)) { 988 assert view.getId() != null; 989 if (firstId == null) { 990 firstId = view.getId(); 991 if (view.isConstrainedVertically()) { 992 // Nothing to do -- we already have an accurate position for 993 // this view 994 } else if (attachTopProperty != null) { 995 view.addVerticalConstraint(attachTopProperty, attachTopValue); 996 if (marginTop > 0) { 997 view.addVerticalConstraint(ATTR_LAYOUT_MARGIN_TOP, 998 String.format(VALUE_N_DP, marginTop)); 999 marginTop = 0; 1000 } 1001 } else { 1002 assert false; 1003 } 1004 } else if (!view.isConstrainedVertically()) { 1005 view.addVerticalConstraint(ATTR_LAYOUT_ALIGN_TOP, firstId); 1006 } 1007 } 1008 } 1009 1010 // Figure out edge for the next row 1011 View view = grid.findBottomEdgeView(row); 1012 if (view != null) { 1013 assert view.getId() != null; 1014 attachTopProperty = ATTR_LAYOUT_BELOW; 1015 attachTopValue = view.getId(); 1016 marginTop = 0; 1017 } else if (marginTop == 0) { 1018 marginTop = grid.getRowHeight(row); 1019 } 1020 } 1021 } 1022 1023 /** 1024 * Searches a view hierarchy and locates the {@link CanvasViewInfo} for the given 1025 * {@link Element} 1026 * 1027 * @param info the root {@link CanvasViewInfo} to search below 1028 * @param element the target element 1029 * @return the {@link CanvasViewInfo} which corresponds to the given element 1030 */ findViewForElement(CanvasViewInfo info, Element element)1031 private CanvasViewInfo findViewForElement(CanvasViewInfo info, Element element) { 1032 if (getElement(info) == element) { 1033 return info; 1034 } 1035 1036 for (CanvasViewInfo child : info.getChildren()) { 1037 CanvasViewInfo result = findViewForElement(child, element); 1038 if (result != null) { 1039 return result; 1040 } 1041 } 1042 1043 return null; 1044 } 1045 1046 /** Returns the {@link Element} for the given {@link CanvasViewInfo} */ getElement(CanvasViewInfo info)1047 private static Element getElement(CanvasViewInfo info) { 1048 Node node = info.getUiViewNode().getXmlNode(); 1049 if (node instanceof Element) { 1050 return (Element) node; 1051 } 1052 1053 return null; 1054 } 1055 1056 /** 1057 * A grid of cells which can contain views, used to infer spatial relationships when 1058 * computing constraints. Note that a view can appear in than one cell; they will 1059 * appear in all cells that their bounds overlap with! 1060 */ 1061 private class Grid { 1062 private final int[] mLeft; 1063 private final int[] mTop; 1064 // A list from row to column to cell, where a cell is a list of views 1065 private final List<List<List<View>>> mRowList; 1066 private int mRowCount; 1067 private int mColCount; 1068 Grid(List<View> views, int[] left, int[] top)1069 Grid(List<View> views, int[] left, int[] top) { 1070 mLeft = left; 1071 mTop = top; 1072 1073 // The left/top arrays should include the ending point too 1074 mColCount = left.length - 1; 1075 mRowCount = top.length - 1; 1076 1077 // Using nested lists rather than arrays to avoid lack of typed arrays 1078 // (can't create List<View>[row][column] arrays) 1079 mRowList = new ArrayList<List<List<View>>>(top.length); 1080 for (int row = 0; row < top.length; row++) { 1081 List<List<View>> columnList = new ArrayList<List<View>>(left.length); 1082 for (int col = 0; col < left.length; col++) { 1083 columnList.add(new ArrayList<View>(4)); 1084 } 1085 mRowList.add(columnList); 1086 } 1087 1088 for (View view : views) { 1089 // Get rid of the root view; we don't want that in the attachments logic; 1090 // it was there originally such that it would contribute the outermost 1091 // edges. 1092 if (view.mElement == mLayout) { 1093 continue; 1094 } 1095 1096 for (int i = 0; i < view.mRowSpan; i++) { 1097 for (int j = 0; j < view.mColSpan; j++) { 1098 mRowList.get(view.mRow + i).get(view.mCol + j).add(view); 1099 } 1100 } 1101 } 1102 } 1103 1104 /** 1105 * Returns the number of rows in the grid 1106 * 1107 * @return the row count 1108 */ getRows()1109 public int getRows() { 1110 return mRowCount; 1111 } 1112 1113 /** 1114 * Returns the number of columns in the grid 1115 * 1116 * @return the column count 1117 */ getColumns()1118 public int getColumns() { 1119 return mColCount; 1120 } 1121 1122 /** 1123 * Returns the list of views overlapping the given cell 1124 * 1125 * @param row the row of the target cell 1126 * @param col the column of the target cell 1127 * @return a list of views overlapping the given column 1128 */ get(int row, int col)1129 public List<View> get(int row, int col) { 1130 return mRowList.get(row).get(col); 1131 } 1132 1133 /** 1134 * Returns true if the given column contains a top left corner of a view 1135 * 1136 * @param column the column to check 1137 * @return true if one or more views have their top left corner in this column 1138 */ colContainsTopLeftCorner(int column)1139 public boolean colContainsTopLeftCorner(int column) { 1140 for (int row = 0; row < mRowCount; row++) { 1141 View view = getTopLeftCorner(row, column); 1142 if (view != null) { 1143 return true; 1144 } 1145 } 1146 1147 return false; 1148 } 1149 1150 /** 1151 * Returns true if the given row contains a top left corner of a view 1152 * 1153 * @param row the row to check 1154 * @return true if one or more views have their top left corner in this row 1155 */ rowContainsTopLeftCorner(int row)1156 public boolean rowContainsTopLeftCorner(int row) { 1157 for (int col = 0; col < mColCount; col++) { 1158 View view = getTopLeftCorner(row, col); 1159 if (view != null) { 1160 return true; 1161 } 1162 } 1163 1164 return false; 1165 } 1166 1167 /** 1168 * Returns a list of views (optionally sorted by increasing row index) that have 1169 * their left edge starting in the given column 1170 * 1171 * @param col the column to look up views for 1172 * @param sort whether to sort the result in increasing row order 1173 * @return a list of views starting in the given column 1174 */ viewsStartingInCol(int col, boolean sort)1175 public List<View> viewsStartingInCol(int col, boolean sort) { 1176 List<View> views = new ArrayList<View>(); 1177 for (int row = 0; row < mRowCount; row++) { 1178 View view = getTopLeftCorner(row, col); 1179 if (view != null) { 1180 views.add(view); 1181 } 1182 } 1183 1184 if (sort) { 1185 View.sortByRow(views); 1186 } 1187 1188 return views; 1189 } 1190 1191 /** 1192 * Returns a list of views (optionally sorted by increasing column index) that have 1193 * their top edge starting in the given row 1194 * 1195 * @param row the row to look up views for 1196 * @param sort whether to sort the result in increasing column order 1197 * @return a list of views starting in the given row 1198 */ viewsStartingInRow(int row, boolean sort)1199 public List<View> viewsStartingInRow(int row, boolean sort) { 1200 List<View> views = new ArrayList<View>(); 1201 for (int col = 0; col < mColCount; col++) { 1202 View view = getTopLeftCorner(row, col); 1203 if (view != null) { 1204 views.add(view); 1205 } 1206 } 1207 1208 if (sort) { 1209 View.sortByColumn(views); 1210 } 1211 1212 return views; 1213 } 1214 1215 /** 1216 * Returns the pixel width of the given column 1217 * 1218 * @param col the column to look up the width of 1219 * @return the width of the column 1220 */ getColumnWidth(int col)1221 public int getColumnWidth(int col) { 1222 return mLeft[col + 1] - mLeft[col]; 1223 } 1224 1225 /** 1226 * Returns the pixel height of the given row 1227 * 1228 * @param row the row to look up the height of 1229 * @return the height of the row 1230 */ getRowHeight(int row)1231 public int getRowHeight(int row) { 1232 return mTop[row + 1] - mTop[row]; 1233 } 1234 1235 /** 1236 * Returns the first view found that has its top left corner in the cell given by 1237 * the row and column indexes, or null if not found. 1238 * 1239 * @param row the row of the target cell 1240 * @param col the column of the target cell 1241 * @return a view with its top left corner in the given cell, or null if not found 1242 */ getTopLeftCorner(int row, int col)1243 View getTopLeftCorner(int row, int col) { 1244 List<View> views = get(row, col); 1245 if (views.size() > 0) { 1246 for (View view : views) { 1247 if (view.mRow == row && view.mCol == col) { 1248 return view; 1249 } 1250 } 1251 } 1252 1253 return null; 1254 } 1255 findRightEdgeView(int col)1256 public View findRightEdgeView(int col) { 1257 for (int row = 0; row < mRowCount; row++) { 1258 List<View> views = get(row, col); 1259 if (views.size() > 0) { 1260 List<View> result = new ArrayList<View>(); 1261 for (View view : views) { 1262 // Ends on the right edge of this column? 1263 if (view.mCol + view.mColSpan == col + 1) { 1264 result.add(view); 1265 } 1266 } 1267 if (result.size() > 1) { 1268 View.sortByColumn(result); 1269 } 1270 if (result.size() > 0) { 1271 return result.get(0); 1272 } 1273 } 1274 } 1275 1276 return null; 1277 } 1278 findBottomEdgeView(int row)1279 public View findBottomEdgeView(int row) { 1280 for (int col = 0; col < mColCount; col++) { 1281 List<View> views = get(row, col); 1282 if (views.size() > 0) { 1283 List<View> result = new ArrayList<View>(); 1284 for (View view : views) { 1285 // Ends on the bottom edge of this column? 1286 if (view.mRow + view.mRowSpan == row + 1) { 1287 result.add(view); 1288 } 1289 } 1290 if (result.size() > 1) { 1291 View.sortByRow(result); 1292 } 1293 if (result.size() > 0) { 1294 return result.get(0); 1295 } 1296 1297 } 1298 } 1299 1300 return null; 1301 } 1302 1303 /** 1304 * Produces a display of view contents along with the pixel positions of each row/column, 1305 * like the following (used for diagnostics only) 1306 * <pre> 1307 * |0 |49 |143 |192 |240 1308 * 36| | |button2 | 1309 * 72| |radioButton1 |button2 | 1310 * 74|button1 |radioButton1 |button2 | 1311 * 108|button1 | |button2 | 1312 * 110| | |button2 | 1313 * 149| | | | 1314 * 320 1315 * </pre> 1316 */ 1317 @Override toString()1318 public String toString() { 1319 // Dump out the view table 1320 int cellWidth = 20; 1321 1322 StringWriter stringWriter = new StringWriter(); 1323 PrintWriter out = new PrintWriter(stringWriter); 1324 out.printf("%" + cellWidth + "s", ""); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ 1325 for (int col = 0; col < mColCount + 1; col++) { 1326 out.printf("|%-" + (cellWidth - 1) + "d", mLeft[col]); //$NON-NLS-1$ //$NON-NLS-2$ 1327 } 1328 out.printf("\n"); //$NON-NLS-1$ 1329 for (int row = 0; row < mRowCount + 1; row++) { 1330 out.printf("%" + cellWidth + "d", mTop[row]); //$NON-NLS-1$ //$NON-NLS-2$ 1331 if (row == mRowCount) { 1332 break; 1333 } 1334 for (int col = 0; col < mColCount; col++) { 1335 List<View> views = get(row, col); 1336 StringBuilder sb = new StringBuilder(); 1337 for (View view : views) { 1338 String id = view != null ? view.getId() : ""; //$NON-NLS-1$ 1339 if (id.startsWith(NEW_ID_PREFIX)) { 1340 id = id.substring(NEW_ID_PREFIX.length()); 1341 } 1342 if (id.length() > cellWidth - 2) { 1343 id = id.substring(0, cellWidth - 2); 1344 } 1345 if (sb.length() > 0) { 1346 sb.append(','); 1347 } 1348 sb.append(id); 1349 } 1350 String cellString = sb.toString(); 1351 if (cellString.contains(",") && cellString.length() > cellWidth - 2) { //$NON-NLS-1$ 1352 cellString = cellString.substring(0, cellWidth - 6) + "...,"; //$NON-NLS-1$ 1353 } 1354 out.printf("|%-" + (cellWidth - 2) + "s ", cellString); //$NON-NLS-1$ //$NON-NLS-2$ 1355 } 1356 out.printf("\n"); //$NON-NLS-1$ 1357 } 1358 1359 out.flush(); 1360 return stringWriter.toString(); 1361 } 1362 } 1363 1364 /** Holds layout information about an individual view. */ 1365 private static class View { 1366 private final Element mElement; 1367 private int mRow = -1; 1368 private int mCol = -1; 1369 private int mRowSpan = -1; 1370 private int mColSpan = -1; 1371 private CanvasViewInfo mInfo; 1372 private String mId; 1373 private List<Pair<String, String>> mHorizConstraints = 1374 new ArrayList<Pair<String, String>>(4); 1375 private List<Pair<String, String>> mVerticalConstraints = 1376 new ArrayList<Pair<String, String>>(4); 1377 private int mGravity; 1378 View(CanvasViewInfo view, Element element)1379 public View(CanvasViewInfo view, Element element) { 1380 mInfo = view; 1381 mElement = element; 1382 mGravity = RelativeLayoutConversionHelper.getGravity(element); 1383 } 1384 getHeight()1385 public int getHeight() { 1386 return mInfo.getAbsRect().height; 1387 } 1388 getGravity()1389 public int getGravity() { 1390 return mGravity; 1391 } 1392 getId()1393 public String getId() { 1394 return mId; 1395 } 1396 getElement()1397 public Element getElement() { 1398 return mElement; 1399 } 1400 getHorizConstraints()1401 public List<Pair<String, String>> getHorizConstraints() { 1402 return mHorizConstraints; 1403 } 1404 getVerticalConstraints()1405 public List<Pair<String, String>> getVerticalConstraints() { 1406 return mVerticalConstraints; 1407 } 1408 isConstrainedHorizontally()1409 public boolean isConstrainedHorizontally() { 1410 return mHorizConstraints.size() > 0; 1411 } 1412 isConstrainedVertically()1413 public boolean isConstrainedVertically() { 1414 return mVerticalConstraints.size() > 0; 1415 } 1416 addHorizConstraint(String property, String value)1417 public void addHorizConstraint(String property, String value) { 1418 assert property != null && value != null; 1419 // TODO - look for duplicates? 1420 mHorizConstraints.add(Pair.of(property, value)); 1421 } 1422 addVerticalConstraint(String property, String value)1423 public void addVerticalConstraint(String property, String value) { 1424 assert property != null && value != null; 1425 mVerticalConstraints.add(Pair.of(property, value)); 1426 } 1427 getLeftEdge()1428 public int getLeftEdge() { 1429 return mInfo.getAbsRect().x; 1430 } 1431 getTopEdge()1432 public int getTopEdge() { 1433 return mInfo.getAbsRect().y; 1434 } 1435 getRightEdge()1436 public int getRightEdge() { 1437 Rectangle bounds = mInfo.getAbsRect(); 1438 // +1: make the bounds overlap, so the right edge is the same as the 1439 // left edge of the neighbor etc. Otherwise we end up with lots of 1-pixel wide 1440 // columns between adjacent items. 1441 return bounds.x + bounds.width + 1; 1442 } 1443 getBottomEdge()1444 public int getBottomEdge() { 1445 Rectangle bounds = mInfo.getAbsRect(); 1446 return bounds.y + bounds.height + 1; 1447 } 1448 1449 @Override toString()1450 public String toString() { 1451 return "View [mId=" + mId + "]"; //$NON-NLS-1$ //$NON-NLS-2$ 1452 } 1453 sortByRow(List<View> views)1454 public static void sortByRow(List<View> views) { 1455 Collections.sort(views, new ViewComparator(true/*rowSort*/)); 1456 } 1457 sortByColumn(List<View> views)1458 public static void sortByColumn(List<View> views) { 1459 Collections.sort(views, new ViewComparator(false/*rowSort*/)); 1460 } 1461 1462 /** Comparator to help sort views by row or column index */ 1463 private static class ViewComparator implements Comparator<View> { 1464 boolean mRowSort; 1465 ViewComparator(boolean rowSort)1466 public ViewComparator(boolean rowSort) { 1467 mRowSort = rowSort; 1468 } 1469 compare(View view1, View view2)1470 public int compare(View view1, View view2) { 1471 if (mRowSort) { 1472 return view1.mRow - view2.mRow; 1473 } else { 1474 return view1.mCol - view2.mCol; 1475 } 1476 } 1477 } 1478 } 1479 1480 /** 1481 * An edge list takes a hierarchy of elements and records the bounds of each element 1482 * into various lists such that it can answer queries about shared edges, about which 1483 * particular pixels occur as a boundary edge, etc. 1484 */ 1485 private class EdgeList { 1486 private final Map<Element, View> mElementToViewMap = new HashMap<Element, View>(100); 1487 private final Map<String, View> mIdToViewMap = new HashMap<String, View>(100); 1488 private final Map<Integer, List<View>> mLeft = new HashMap<Integer, List<View>>(); 1489 private final Map<Integer, List<View>> mTop = new HashMap<Integer, List<View>>(); 1490 private final Map<Integer, List<View>> mRight = new HashMap<Integer, List<View>>(); 1491 private final Map<Integer, List<View>> mBottom = new HashMap<Integer, List<View>>(); 1492 private final Map<Element, Element> mSharedLeftEdge = new HashMap<Element, Element>(); 1493 private final Map<Element, Element> mSharedTopEdge = new HashMap<Element, Element>(); 1494 private final Map<Element, Element> mSharedRightEdge = new HashMap<Element, Element>(); 1495 private final Map<Element, Element> mSharedBottomEdge = new HashMap<Element, Element>(); 1496 private final List<Element> mDelete = new ArrayList<Element>(); 1497 EdgeList(CanvasViewInfo view)1498 EdgeList(CanvasViewInfo view) { 1499 analyze(view, true); 1500 mDelete.remove(getElement(view)); 1501 } 1502 setIdAttributeValue(View view, String id)1503 public void setIdAttributeValue(View view, String id) { 1504 assert id.startsWith(NEW_ID_PREFIX) || id.startsWith(ID_PREFIX); 1505 view.mId = id; 1506 mIdToViewMap.put(getIdBasename(id), view); 1507 } 1508 getView(Element element)1509 public View getView(Element element) { 1510 return mElementToViewMap.get(element); 1511 } 1512 getView(String id)1513 public View getView(String id) { 1514 return mIdToViewMap.get(id); 1515 } 1516 getTopEdgeViews(Integer topOffset)1517 public List<View> getTopEdgeViews(Integer topOffset) { 1518 return mTop.get(topOffset); 1519 } 1520 getLeftEdgeViews(Integer leftOffset)1521 public List<View> getLeftEdgeViews(Integer leftOffset) { 1522 return mLeft.get(leftOffset); 1523 } 1524 record(Map<Integer, List<View>> map, Integer edge, View info)1525 void record(Map<Integer, List<View>> map, Integer edge, View info) { 1526 List<View> list = map.get(edge); 1527 if (list == null) { 1528 list = new ArrayList<View>(); 1529 map.put(edge, list); 1530 } 1531 list.add(info); 1532 } 1533 getOffsets(Set<Integer> first, Set<Integer> second)1534 private List<Integer> getOffsets(Set<Integer> first, Set<Integer> second) { 1535 Set<Integer> joined = new HashSet<Integer>(first.size() + second.size()); 1536 joined.addAll(first); 1537 joined.addAll(second); 1538 List<Integer> unique = new ArrayList<Integer>(joined); 1539 Collections.sort(unique); 1540 1541 return unique; 1542 } 1543 getDeletedElements()1544 public List<Element> getDeletedElements() { 1545 return mDelete; 1546 } 1547 getColumnOffsets()1548 public List<Integer> getColumnOffsets() { 1549 return getOffsets(mLeft.keySet(), mRight.keySet()); 1550 } getRowOffsets()1551 public List<Integer> getRowOffsets() { 1552 return getOffsets(mTop.keySet(), mBottom.keySet()); 1553 } 1554 analyze(CanvasViewInfo view, boolean isRoot)1555 private View analyze(CanvasViewInfo view, boolean isRoot) { 1556 View added = null; 1557 if (!mFlatten || !isRemovableLayout(view)) { 1558 added = add(view); 1559 if (!isRoot) { 1560 return added; 1561 } 1562 } else { 1563 mDelete.add(getElement(view)); 1564 } 1565 1566 Element parentElement = getElement(view); 1567 Rectangle parentBounds = view.getAbsRect(); 1568 1569 // Build up a table model of the view 1570 for (CanvasViewInfo child : view.getChildren()) { 1571 Rectangle childBounds = child.getAbsRect(); 1572 Element childElement = getElement(child); 1573 1574 // See if this view shares the edge with the removed 1575 // parent layout, and if so, record that such that we can 1576 // later handle attachments to the removed parent edges 1577 if (parentBounds.x == childBounds.x) { 1578 mSharedLeftEdge.put(childElement, parentElement); 1579 } 1580 if (parentBounds.y == childBounds.y) { 1581 mSharedTopEdge.put(childElement, parentElement); 1582 } 1583 if (parentBounds.x + parentBounds.width == childBounds.x + childBounds.width) { 1584 mSharedRightEdge.put(childElement, parentElement); 1585 } 1586 if (parentBounds.y + parentBounds.height == childBounds.y + childBounds.height) { 1587 mSharedBottomEdge.put(childElement, parentElement); 1588 } 1589 1590 if (mFlatten && isRemovableLayout(child)) { 1591 // When flattening, we want to disregard all layouts and instead 1592 // add their children! 1593 for (CanvasViewInfo childView : child.getChildren()) { 1594 analyze(childView, false); 1595 1596 Element childViewElement = getElement(childView); 1597 Rectangle childViewBounds = childView.getAbsRect(); 1598 1599 // See if this view shares the edge with the removed 1600 // parent layout, and if so, record that such that we can 1601 // later handle attachments to the removed parent edges 1602 if (parentBounds.x == childViewBounds.x) { 1603 mSharedLeftEdge.put(childViewElement, parentElement); 1604 } 1605 if (parentBounds.y == childViewBounds.y) { 1606 mSharedTopEdge.put(childViewElement, parentElement); 1607 } 1608 if (parentBounds.x + parentBounds.width == childViewBounds.x 1609 + childViewBounds.width) { 1610 mSharedRightEdge.put(childViewElement, parentElement); 1611 } 1612 if (parentBounds.y + parentBounds.height == childViewBounds.y 1613 + childViewBounds.height) { 1614 mSharedBottomEdge.put(childViewElement, parentElement); 1615 } 1616 } 1617 mDelete.add(childElement); 1618 } else { 1619 analyze(child, false); 1620 } 1621 } 1622 1623 return added; 1624 } 1625 getSharedLeftEdge(Element element)1626 public View getSharedLeftEdge(Element element) { 1627 return getSharedEdge(element, mSharedLeftEdge); 1628 } 1629 getSharedRightEdge(Element element)1630 public View getSharedRightEdge(Element element) { 1631 return getSharedEdge(element, mSharedRightEdge); 1632 } 1633 getSharedTopEdge(Element element)1634 public View getSharedTopEdge(Element element) { 1635 return getSharedEdge(element, mSharedTopEdge); 1636 } 1637 getSharedBottomEdge(Element element)1638 public View getSharedBottomEdge(Element element) { 1639 return getSharedEdge(element, mSharedBottomEdge); 1640 } 1641 getSharedEdge(Element element, Map<Element, Element> sharedEdgeMap)1642 private View getSharedEdge(Element element, Map<Element, Element> sharedEdgeMap) { 1643 Element original = element; 1644 1645 while (element != null) { 1646 View view = getView(element); 1647 if (view != null) { 1648 assert isAncestor(element, original); 1649 return view; 1650 } 1651 element = sharedEdgeMap.get(element); 1652 } 1653 1654 return null; 1655 } 1656 add(CanvasViewInfo info)1657 private View add(CanvasViewInfo info) { 1658 Rectangle bounds = info.getAbsRect(); 1659 Element element = getElement(info); 1660 View view = new View(info, element); 1661 mElementToViewMap.put(element, view); 1662 record(mLeft, Integer.valueOf(bounds.x), view); 1663 record(mTop, Integer.valueOf(bounds.y), view); 1664 record(mRight, Integer.valueOf(view.getRightEdge()), view); 1665 record(mBottom, Integer.valueOf(view.getBottomEdge()), view); 1666 return view; 1667 } 1668 1669 /** 1670 * Returns true if the given {@link CanvasViewInfo} represents an element we 1671 * should remove in a flattening conversion. We don't want to remove non-layout 1672 * views, or layout views that for example contain drawables on their own. 1673 */ isRemovableLayout(CanvasViewInfo child)1674 private boolean isRemovableLayout(CanvasViewInfo child) { 1675 // The element being converted is NOT removable! 1676 Element element = getElement(child); 1677 if (element == mLayout) { 1678 return false; 1679 } 1680 1681 ElementDescriptor descriptor = child.getUiViewNode().getDescriptor(); 1682 String name = descriptor.getXmlLocalName(); 1683 if (name.equals(LINEAR_LAYOUT) || name.equals(RELATIVE_LAYOUT)) { 1684 // Don't delete layouts that provide a background image or gradient 1685 if (element.hasAttributeNS(ANDROID_URI, ATTR_BACKGROUND)) { 1686 AdtPlugin.log(IStatus.WARNING, 1687 "Did not flatten layout %1$s because it defines a '%2$s' attribute", 1688 VisualRefactoring.getId(element), ATTR_BACKGROUND); 1689 return false; 1690 } 1691 1692 return true; 1693 } 1694 1695 return false; 1696 } 1697 } 1698 } 1699