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