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