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_COLUMN_COUNT; 21 import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_BASELINE; 22 import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_BOTTOM; 23 import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_LEFT; 24 import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_RIGHT; 25 import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_TOP; 26 import static com.android.SdkConstants.ATTR_LAYOUT_COLUMN; 27 import static com.android.SdkConstants.ATTR_LAYOUT_COLUMN_SPAN; 28 import static com.android.SdkConstants.ATTR_LAYOUT_GRAVITY; 29 import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT; 30 import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX; 31 import static com.android.SdkConstants.ATTR_LAYOUT_ROW; 32 import static com.android.SdkConstants.ATTR_LAYOUT_ROW_SPAN; 33 import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH; 34 import static com.android.SdkConstants.ATTR_ORIENTATION; 35 import static com.android.SdkConstants.FQCN_GRID_LAYOUT; 36 import static com.android.SdkConstants.FQCN_SPACE; 37 import static com.android.SdkConstants.GRAVITY_VALUE_FILL; 38 import static com.android.SdkConstants.GRAVITY_VALUE_FILL_HORIZONTAL; 39 import static com.android.SdkConstants.GRAVITY_VALUE_FILL_VERTICAL; 40 import static com.android.SdkConstants.ID_PREFIX; 41 import static com.android.SdkConstants.LINEAR_LAYOUT; 42 import static com.android.SdkConstants.NEW_ID_PREFIX; 43 import static com.android.SdkConstants.RADIO_GROUP; 44 import static com.android.SdkConstants.RELATIVE_LAYOUT; 45 import static com.android.SdkConstants.SPACE; 46 import static com.android.SdkConstants.TABLE_LAYOUT; 47 import static com.android.SdkConstants.TABLE_ROW; 48 import static com.android.SdkConstants.VALUE_FILL_PARENT; 49 import static com.android.SdkConstants.VALUE_HORIZONTAL; 50 import static com.android.SdkConstants.VALUE_MATCH_PARENT; 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_HORIZ_MASK; 54 import static com.android.ide.common.layout.GravityHelper.GRAVITY_VERT_MASK; 55 56 import com.android.ide.common.api.IViewMetadata.FillPreference; 57 import com.android.ide.common.layout.BaseLayoutRule; 58 import com.android.ide.common.layout.GravityHelper; 59 import com.android.ide.common.layout.GridLayoutRule; 60 import com.android.ide.eclipse.adt.AdtPlugin; 61 import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor; 62 import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor; 63 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor; 64 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo; 65 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities; 66 import com.android.ide.eclipse.adt.internal.editors.layout.gre.ViewMetadataRepository; 67 import com.android.ide.eclipse.adt.internal.project.SupportLibraryHelper; 68 69 import org.eclipse.core.resources.IFile; 70 import org.eclipse.core.runtime.IStatus; 71 import org.eclipse.swt.graphics.Rectangle; 72 import org.eclipse.text.edits.InsertEdit; 73 import org.eclipse.text.edits.MalformedTreeException; 74 import org.eclipse.text.edits.MultiTextEdit; 75 import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion; 76 import org.w3c.dom.Attr; 77 import org.w3c.dom.Element; 78 import org.w3c.dom.NamedNodeMap; 79 import org.w3c.dom.Node; 80 81 import java.util.ArrayList; 82 import java.util.Collection; 83 import java.util.Collections; 84 import java.util.HashMap; 85 import java.util.HashSet; 86 import java.util.Iterator; 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 grid layout 93 * <p> 94 * Future enhancements: 95 * <ul> 96 * <li>Render the layout at multiple screen sizes and analyze how the widget bounds 97 * change and use this to infer gravity 98 * <li> Use the layout_width and layout_height attributes on views to infer column and 99 * row flexibility (and as mentioned above, possibly layout_weight). 100 * move and stretch and use that to add in additional constraints 101 * <li> Take into account existing margins and add/subtract those from the 102 * bounds computations and either clear or update them. 103 * <li>Try to reorder elements into their natural order 104 * <li> Try to preserve spacing? Right now everything gets converted into a compact 105 * grid with no spacing between the views; consider inserting {@code <Space>} views 106 * with dimensions based on existing distances. 107 * </ul> 108 */ 109 @SuppressWarnings("restriction") // DOM model access 110 class GridLayoutConverter { 111 private final MultiTextEdit mRootEdit; 112 private final boolean mFlatten; 113 private final Element mLayout; 114 private final ChangeLayoutRefactoring mRefactoring; 115 private final CanvasViewInfo mRootView; 116 117 private List<View> mViews; 118 private String mNamespace; 119 private int mColumnCount; 120 121 /** Creates a new {@link GridLayoutConverter} */ GridLayoutConverter(ChangeLayoutRefactoring refactoring, Element layout, boolean flatten, MultiTextEdit rootEdit, CanvasViewInfo rootView)122 GridLayoutConverter(ChangeLayoutRefactoring refactoring, 123 Element layout, boolean flatten, MultiTextEdit rootEdit, CanvasViewInfo rootView) { 124 mRefactoring = refactoring; 125 mLayout = layout; 126 mFlatten = flatten; 127 mRootEdit = rootEdit; 128 mRootView = rootView; 129 } 130 131 /** Performs conversion from any layout to a RelativeLayout */ convertToGridLayout()132 public void convertToGridLayout() { 133 if (mRootView == null) { 134 return; 135 } 136 137 // Locate the view for the layout 138 CanvasViewInfo layoutView = findViewForElement(mRootView, mLayout); 139 if (layoutView == null || layoutView.getChildren().size() == 0) { 140 // No children. THAT was an easy conversion! 141 return; 142 } 143 144 // Study the layout and get information about how to place individual elements 145 GridModel gridModel = new GridModel(layoutView, mLayout, mFlatten); 146 mViews = gridModel.getViews(); 147 mColumnCount = gridModel.computeColumnCount(); 148 149 deleteRemovedElements(gridModel.getDeletedElements()); 150 mNamespace = mRefactoring.getAndroidNamespacePrefix(); 151 152 processGravities(); 153 154 // Insert space views if necessary 155 insertStretchableSpans(); 156 157 // Create/update relative layout constraints 158 assignGridAttributes(); 159 160 removeUndefinedAttrs(); 161 162 if (mColumnCount > 0) { 163 mRefactoring.setAttribute(mRootEdit, mLayout, ANDROID_URI, 164 mNamespace, ATTR_COLUMN_COUNT, Integer.toString(mColumnCount)); 165 } 166 } 167 insertStretchableSpans()168 private void insertStretchableSpans() { 169 // Look at the rows and columns and determine if we need to have a stretchable 170 // row and/or a stretchable column in the layout. 171 // In a GridLayout, a row or column is stretchable if it defines a gravity (regardless 172 // of what the gravity is -- in other words, a column is not just stretchable if it 173 // has gravity=fill but also if it has gravity=left). Furthermore, ALL the elements 174 // in the row/column have to be stretchable for the overall row/column to be 175 // considered stretchable. 176 177 // Map from row index to boolean for "is the row fixed/inflexible?" 178 Map<Integer, Boolean> rowFixed = new HashMap<Integer, Boolean>(); 179 Map<Integer, Boolean> columnFixed = new HashMap<Integer, Boolean>(); 180 for (View view : mViews) { 181 if (view.mElement == mLayout) { 182 continue; 183 } 184 185 int gravity = GravityHelper.getGravity(view.mGravity, 0); 186 if ((gravity & GRAVITY_HORIZ_MASK) == 0) { 187 columnFixed.put(view.mCol, true); 188 } else if (!columnFixed.containsKey(view.mCol)) { 189 columnFixed.put(view.mCol, false); 190 } 191 if ((gravity & GRAVITY_VERT_MASK) == 0) { 192 rowFixed.put(view.mRow, true); 193 } else if (!rowFixed.containsKey(view.mRow)) { 194 rowFixed.put(view.mRow, false); 195 } 196 } 197 198 boolean hasStretchableRow = false; 199 boolean hasStretchableColumn = false; 200 for (boolean fixed : rowFixed.values()) { 201 if (!fixed) { 202 hasStretchableRow = true; 203 } 204 } 205 for (boolean fixed : columnFixed.values()) { 206 if (!fixed) { 207 hasStretchableColumn = true; 208 } 209 } 210 211 if (!hasStretchableRow || !hasStretchableColumn) { 212 // Insert <Space> to hold stretchable space 213 // TODO: May also have to increment column count! 214 int offset = 0; // WHERE? 215 216 String gridLayout = mLayout.getTagName(); 217 if (mLayout instanceof IndexedRegion) { 218 IndexedRegion region = (IndexedRegion) mLayout; 219 int end = region.getEndOffset(); 220 // TODO: Look backwards for the "</" 221 // (and can it ever be <foo/>) ? 222 end -= (gridLayout.length() + 3); // 3: <, /, > 223 offset = end; 224 } 225 226 int row = rowFixed.size(); 227 int column = columnFixed.size(); 228 StringBuilder sb = new StringBuilder(64); 229 String spaceTag = SPACE; 230 IFile file = mRefactoring.getFile(); 231 if (file != null) { 232 spaceTag = SupportLibraryHelper.getTagFor(file.getProject(), FQCN_SPACE); 233 if (spaceTag.equals(FQCN_SPACE)) { 234 spaceTag = SPACE; 235 } 236 } 237 238 sb.append('<').append(spaceTag).append(' '); 239 String gravity; 240 if (!hasStretchableRow && !hasStretchableColumn) { 241 gravity = GRAVITY_VALUE_FILL; 242 } else if (!hasStretchableRow) { 243 gravity = GRAVITY_VALUE_FILL_VERTICAL; 244 } else { 245 assert !hasStretchableColumn; 246 gravity = GRAVITY_VALUE_FILL_HORIZONTAL; 247 } 248 249 sb.append(mNamespace).append(':'); 250 sb.append(ATTR_LAYOUT_GRAVITY).append('=').append('"').append(gravity); 251 sb.append('"').append(' '); 252 253 sb.append(mNamespace).append(':'); 254 sb.append(ATTR_LAYOUT_ROW).append('=').append('"').append(Integer.toString(row)); 255 sb.append('"').append(' '); 256 257 sb.append(mNamespace).append(':'); 258 sb.append(ATTR_LAYOUT_COLUMN).append('=').append('"').append(Integer.toString(column)); 259 sb.append('"').append('/').append('>'); 260 261 String space = sb.toString(); 262 InsertEdit replace = new InsertEdit(offset, space); 263 mRootEdit.addChild(replace); 264 265 mColumnCount++; 266 } 267 } 268 removeUndefinedAttrs()269 private void removeUndefinedAttrs() { 270 ViewElementDescriptor descriptor = mRefactoring.getElementDescriptor(FQCN_GRID_LAYOUT); 271 if (descriptor == null) { 272 return; 273 } 274 275 Set<String> defined = new HashSet<String>(); 276 AttributeDescriptor[] layoutAttributes = descriptor.getLayoutAttributes(); 277 for (AttributeDescriptor attribute : layoutAttributes) { 278 defined.add(attribute.getXmlLocalName()); 279 } 280 281 for (View view : mViews) { 282 Element child = view.mElement; 283 284 List<Attr> attributes = mRefactoring.findLayoutAttributes(child); 285 for (Attr attribute : attributes) { 286 String name = attribute.getLocalName(); 287 if (!defined.contains(name)) { 288 // Remove it 289 try { 290 mRefactoring.removeAttribute(mRootEdit, child, attribute.getNamespaceURI(), 291 name); 292 } catch (MalformedTreeException mte) { 293 // Sometimes refactoring has modified attribute; not 294 // removing 295 // it is non-fatal so just warn instead of letting 296 // refactoring 297 // operation abort 298 AdtPlugin.log(IStatus.WARNING, 299 "Could not remove unsupported attribute %1$s; " + //$NON-NLS-1$ 300 "already modified during refactoring?", //$NON-NLS-1$ 301 attribute.getLocalName()); 302 } 303 } 304 } 305 } 306 } 307 308 /** Removes any elements targeted for deletion */ deleteRemovedElements(List<Element> delete)309 private void deleteRemovedElements(List<Element> delete) { 310 if (mFlatten && delete.size() > 0) { 311 for (Element element : delete) { 312 mRefactoring.removeElementTags(mRootEdit, element, delete, 313 false /*changeIndentation*/); 314 } 315 } 316 } 317 318 /** 319 * Creates refactoring edits which adds or updates the grid attributes 320 */ assignGridAttributes()321 private void assignGridAttributes() { 322 // We always convert to horizontal grid layouts for now 323 mRefactoring.setAttribute(mRootEdit, mLayout, ANDROID_URI, 324 mNamespace, ATTR_ORIENTATION, VALUE_HORIZONTAL); 325 326 assignCellAttributes(); 327 } 328 329 /** 330 * Assign cell attributes to the table, skipping those that will be implied 331 * by the grid model 332 */ assignCellAttributes()333 private void assignCellAttributes() { 334 int implicitRow = 0; 335 int implicitColumn = 0; 336 int nextRow = 0; 337 for (View view : mViews) { 338 Element element = view.getElement(); 339 if (element == mLayout) { 340 continue; 341 } 342 343 int row = view.getRow(); 344 int column = view.getColumn(); 345 346 if (column != implicitColumn && (implicitColumn > 0 || implicitRow > 0)) { 347 mRefactoring.setAttribute(mRootEdit, element, ANDROID_URI, 348 mNamespace, ATTR_LAYOUT_COLUMN, Integer.toString(column)); 349 if (column < implicitColumn) { 350 implicitRow++; 351 } 352 implicitColumn = column; 353 } 354 if (row != implicitRow) { 355 mRefactoring.setAttribute(mRootEdit, element, ANDROID_URI, 356 mNamespace, ATTR_LAYOUT_ROW, Integer.toString(row)); 357 implicitRow = row; 358 } 359 360 int rowSpan = view.getRowSpan(); 361 int columnSpan = view.getColumnSpan(); 362 assert columnSpan >= 1; 363 364 if (rowSpan > 1) { 365 mRefactoring.setAttribute(mRootEdit, element, ANDROID_URI, 366 mNamespace, ATTR_LAYOUT_ROW_SPAN, Integer.toString(rowSpan)); 367 } 368 if (columnSpan > 1) { 369 mRefactoring.setAttribute(mRootEdit, element, ANDROID_URI, 370 mNamespace, ATTR_LAYOUT_COLUMN_SPAN, 371 Integer.toString(columnSpan)); 372 } 373 nextRow = Math.max(nextRow, row + rowSpan); 374 375 // wrap_content is redundant in GridLayouts 376 Attr width = element.getAttributeNodeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH); 377 if (width != null && VALUE_WRAP_CONTENT.equals(width.getValue())) { 378 mRefactoring.removeAttribute(mRootEdit, width); 379 } 380 Attr height = element.getAttributeNodeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT); 381 if (height != null && VALUE_WRAP_CONTENT.equals(height.getValue())) { 382 mRefactoring.removeAttribute(mRootEdit, height); 383 } 384 385 // Fix up children moved from LinearLayouts that have "invalid" sizes that 386 // was intended for layout weight handling in their old parent 387 if (LINEAR_LAYOUT.equals(element.getParentNode().getNodeName())) { 388 convert0dipToWrapContent(element); 389 } 390 391 implicitColumn += columnSpan; 392 if (implicitColumn >= mColumnCount) { 393 implicitColumn = 0; 394 assert nextRow > implicitRow; 395 implicitRow = nextRow; 396 } 397 } 398 } 399 processGravities()400 private void processGravities() { 401 for (View view : mViews) { 402 Element element = view.getElement(); 403 if (element == mLayout) { 404 continue; 405 } 406 407 Attr width = element.getAttributeNodeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH); 408 Attr height = element.getAttributeNodeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT); 409 String gravity = element.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_GRAVITY); 410 String newGravity = null; 411 if (width != null && (VALUE_MATCH_PARENT.equals(width.getValue()) || 412 VALUE_FILL_PARENT.equals(width.getValue()))) { 413 mRefactoring.removeAttribute(mRootEdit, width); 414 newGravity = gravity = GRAVITY_VALUE_FILL_HORIZONTAL; 415 } 416 if (height != null && (VALUE_MATCH_PARENT.equals(height.getValue()) || 417 VALUE_FILL_PARENT.equals(height.getValue()))) { 418 mRefactoring.removeAttribute(mRootEdit, height); 419 if (newGravity == GRAVITY_VALUE_FILL_HORIZONTAL) { 420 newGravity = GRAVITY_VALUE_FILL; 421 } else { 422 newGravity = GRAVITY_VALUE_FILL_VERTICAL; 423 } 424 gravity = newGravity; 425 } 426 427 if (gravity == null || gravity.length() == 0) { 428 ElementDescriptor descriptor = view.mInfo.getUiViewNode().getDescriptor(); 429 if (descriptor instanceof ViewElementDescriptor) { 430 ViewElementDescriptor viewDescriptor = (ViewElementDescriptor) descriptor; 431 String fqcn = viewDescriptor.getFullClassName(); 432 FillPreference fill = ViewMetadataRepository.get().getFillPreference(fqcn); 433 gravity = GridLayoutRule.computeDefaultGravity(fill); 434 if (gravity != null) { 435 newGravity = gravity; 436 } 437 } 438 } 439 440 if (newGravity != null) { 441 mRefactoring.setAttribute(mRootEdit, element, ANDROID_URI, 442 mNamespace, ATTR_LAYOUT_GRAVITY, newGravity); 443 } 444 445 view.mGravity = newGravity != null ? newGravity : gravity; 446 } 447 } 448 449 450 /** Converts 0dip values in layout_width and layout_height to wrap_content instead */ convert0dipToWrapContent(Element child)451 private void convert0dipToWrapContent(Element child) { 452 // Must convert layout_height="0dip" to layout_height="wrap_content". 453 // (And since wrap_content is the default, what we really do is remove 454 // the attribute completely.) 455 // 0dip is a special trick used in linear layouts in the presence of 456 // weights where 0dip ensures that the height of the view is not taken 457 // into account when distributing the weights. However, when converted 458 // to RelativeLayout this will instead cause the view to actually be assigned 459 // 0 height. 460 Attr height = child.getAttributeNodeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT); 461 // 0dip, 0dp, 0px, etc 462 if (height != null && height.getValue().startsWith("0")) { //$NON-NLS-1$ 463 mRefactoring.removeAttribute(mRootEdit, height); 464 } 465 Attr width = child.getAttributeNodeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH); 466 if (width != null && width.getValue().startsWith("0")) { //$NON-NLS-1$ 467 mRefactoring.removeAttribute(mRootEdit, width); 468 } 469 } 470 471 /** 472 * Searches a view hierarchy and locates the {@link CanvasViewInfo} for the given 473 * {@link Element} 474 * 475 * @param info the root {@link CanvasViewInfo} to search below 476 * @param element the target element 477 * @return the {@link CanvasViewInfo} which corresponds to the given element 478 */ findViewForElement(CanvasViewInfo info, Element element)479 private CanvasViewInfo findViewForElement(CanvasViewInfo info, Element element) { 480 if (getElement(info) == element) { 481 return info; 482 } 483 484 for (CanvasViewInfo child : info.getChildren()) { 485 CanvasViewInfo result = findViewForElement(child, element); 486 if (result != null) { 487 return result; 488 } 489 } 490 491 return null; 492 } 493 494 /** Returns the {@link Element} for the given {@link CanvasViewInfo} */ getElement(CanvasViewInfo info)495 private static Element getElement(CanvasViewInfo info) { 496 Node node = info.getUiViewNode().getXmlNode(); 497 if (node instanceof Element) { 498 return (Element) node; 499 } 500 501 return null; 502 } 503 504 505 /** Holds layout information about an individual view */ 506 private static class View { 507 private final Element mElement; 508 private int mRow = -1; 509 private int mCol = -1; 510 private int mRowSpan = -1; 511 private int mColSpan = -1; 512 private int mX1; 513 private int mY1; 514 private int mX2; 515 private int mY2; 516 private CanvasViewInfo mInfo; 517 private String mGravity; 518 View(CanvasViewInfo view, Element element)519 public View(CanvasViewInfo view, Element element) { 520 mInfo = view; 521 mElement = element; 522 523 Rectangle b = mInfo.getAbsRect(); 524 mX1 = b.x; 525 mX2 = b.x + b.width; 526 mY1 = b.y; 527 mY2 = b.y + b.height; 528 } 529 530 /** 531 * Returns the element for this view 532 * 533 * @return the element for the view 534 */ getElement()535 public Element getElement() { 536 return mElement; 537 } 538 539 /** 540 * The assigned row for this view 541 * 542 * @return the assigned row 543 */ getRow()544 public int getRow() { 545 return mRow; 546 } 547 548 /** 549 * The assigned column for this view 550 * 551 * @return the assigned column 552 */ getColumn()553 public int getColumn() { 554 return mCol; 555 } 556 557 /** 558 * The assigned row span for this view 559 * 560 * @return the assigned row span 561 */ getRowSpan()562 public int getRowSpan() { 563 return mRowSpan; 564 } 565 566 /** 567 * The assigned column span for this view 568 * 569 * @return the assigned column span 570 */ getColumnSpan()571 public int getColumnSpan() { 572 return mColSpan; 573 } 574 575 /** 576 * The left edge of the view to be used for placement 577 * 578 * @return the left edge x coordinate 579 */ getLeftEdge()580 public int getLeftEdge() { 581 return mX1; 582 } 583 584 /** 585 * The top edge of the view to be used for placement 586 * 587 * @return the top edge y coordinate 588 */ getTopEdge()589 public int getTopEdge() { 590 return mY1; 591 } 592 593 /** 594 * The right edge of the view to be used for placement 595 * 596 * @return the right edge x coordinate 597 */ getRightEdge()598 public int getRightEdge() { 599 return mX2; 600 } 601 602 /** 603 * The bottom edge of the view to be used for placement 604 * 605 * @return the bottom edge y coordinate 606 */ getBottomEdge()607 public int getBottomEdge() { 608 return mY2; 609 } 610 611 @Override toString()612 public String toString() { 613 return "View(" + VisualRefactoring.getId(mElement) + ": " + mX1 + "," + mY1 + ")"; 614 } 615 } 616 617 /** Grid model for the views found in the view hierarchy, partitioned into rows and columns */ 618 private static class GridModel { 619 private final List<View> mViews = new ArrayList<View>(); 620 private final List<Element> mDelete = new ArrayList<Element>(); 621 private final Map<Element, View> mElementToView = new HashMap<Element, View>(); 622 private Element mLayout; 623 private boolean mFlatten; 624 GridModel(CanvasViewInfo view, Element layout, boolean flatten)625 GridModel(CanvasViewInfo view, Element layout, boolean flatten) { 626 mLayout = layout; 627 mFlatten = flatten; 628 629 scan(view, true); 630 analyzeKnownLayouts(); 631 initializeColumns(); 632 initializeRows(); 633 mDelete.remove(getElement(view)); 634 } 635 636 /** 637 * Returns the {@link View} objects to be placed in the grid 638 * 639 * @return list of {@link View} objects, never null but possibly empty 640 */ getViews()641 public List<View> getViews() { 642 return mViews; 643 } 644 645 /** 646 * Returns the list of elements that are scheduled for deletion in the 647 * flattening operation 648 * 649 * @return elements to be deleted, never null but possibly empty 650 */ getDeletedElements()651 public List<Element> getDeletedElements() { 652 return mDelete; 653 } 654 655 /** 656 * Compute and return column count 657 * 658 * @return the column count 659 */ computeColumnCount()660 public int computeColumnCount() { 661 int columnCount = 0; 662 for (View view : mViews) { 663 if (view.getElement() == mLayout) { 664 continue; 665 } 666 667 int column = view.getColumn(); 668 int columnSpan = view.getColumnSpan(); 669 if (column + columnSpan > columnCount) { 670 columnCount = column + columnSpan; 671 } 672 } 673 return columnCount; 674 } 675 676 /** 677 * Initializes the column and columnSpan attributes of the views 678 */ initializeColumns()679 private void initializeColumns() { 680 // Now initialize table view row, column and spans 681 Map<Integer, List<View>> mColumnViews = new HashMap<Integer, List<View>>(); 682 for (View view : mViews) { 683 if (view.mElement == mLayout) { 684 continue; 685 } 686 int x = view.getLeftEdge(); 687 List<View> list = mColumnViews.get(x); 688 if (list == null) { 689 list = new ArrayList<View>(); 690 mColumnViews.put(x, list); 691 } 692 list.add(view); 693 } 694 695 List<Integer> columnOffsets = new ArrayList<Integer>(mColumnViews.keySet()); 696 Collections.sort(columnOffsets); 697 698 int columnIndex = 0; 699 for (Integer column : columnOffsets) { 700 List<View> views = mColumnViews.get(column); 701 if (views != null) { 702 for (View view : views) { 703 view.mCol = columnIndex; 704 } 705 } 706 columnIndex++; 707 } 708 // Initialize column spans 709 for (View view : mViews) { 710 if (view.mElement == mLayout) { 711 continue; 712 } 713 int index = Collections.binarySearch(columnOffsets, view.getRightEdge()); 714 int column; 715 if (index == -1) { 716 // Smaller than the first element; just use the first column 717 column = 0; 718 } else if (index < 0) { 719 column = -(index + 2); 720 } else { 721 column = index; 722 } 723 724 if (column < view.mCol) { 725 column = view.mCol; 726 } 727 728 view.mColSpan = column - view.mCol + 1; 729 } 730 } 731 732 /** 733 * Initializes the row and rowSpan attributes of the views 734 */ initializeRows()735 private void initializeRows() { 736 Map<Integer, List<View>> mRowViews = new HashMap<Integer, List<View>>(); 737 for (View view : mViews) { 738 if (view.mElement == mLayout) { 739 continue; 740 } 741 int y = view.getTopEdge(); 742 List<View> list = mRowViews.get(y); 743 if (list == null) { 744 list = new ArrayList<View>(); 745 mRowViews.put(y, list); 746 } 747 list.add(view); 748 } 749 750 List<Integer> rowOffsets = new ArrayList<Integer>(mRowViews.keySet()); 751 Collections.sort(rowOffsets); 752 753 int rowIndex = 0; 754 for (Integer row : rowOffsets) { 755 List<View> views = mRowViews.get(row); 756 if (views != null) { 757 for (View view : views) { 758 view.mRow = rowIndex; 759 } 760 } 761 rowIndex++; 762 } 763 764 // Initialize row spans 765 for (View view : mViews) { 766 if (view.mElement == mLayout) { 767 continue; 768 } 769 int index = Collections.binarySearch(rowOffsets, view.getBottomEdge()); 770 int row; 771 if (index == -1) { 772 // Smaller than the first element; just use the first row 773 row = 0; 774 } else if (index < 0) { 775 row = -(index + 2); 776 } else { 777 row = index; 778 } 779 780 if (row < view.mRow) { 781 row = view.mRow; 782 } 783 784 view.mRowSpan = row - view.mRow + 1; 785 } 786 } 787 788 /** 789 * Walks over a given view hierarchy and locates views to be placed in 790 * the grid layout (or deleted if we are flattening the hierarchy) 791 * 792 * @param view the view to analyze 793 * @param isRoot whether this view is the root (which cannot be removed) 794 * @return the {@link View} object for the {@link CanvasViewInfo} 795 * hierarchy we just analyzed, or null 796 */ scan(CanvasViewInfo view, boolean isRoot)797 private View scan(CanvasViewInfo view, boolean isRoot) { 798 View added = null; 799 if (!mFlatten || !isRemovableLayout(view)) { 800 added = add(view); 801 if (!isRoot) { 802 return added; 803 } 804 } else { 805 mDelete.add(getElement(view)); 806 } 807 808 // Build up a table model of the view 809 for (CanvasViewInfo child : view.getChildren()) { 810 Element childElement = getElement(child); 811 812 // See if this view shares the edge with the removed 813 // parent layout, and if so, record that such that we can 814 // later handle attachments to the removed parent edges 815 816 if (mFlatten && isRemovableLayout(child)) { 817 // When flattening, we want to disregard all layouts and instead 818 // add their children! 819 for (CanvasViewInfo childView : child.getChildren()) { 820 scan(childView, false); 821 } 822 mDelete.add(childElement); 823 } else { 824 scan(child, false); 825 } 826 } 827 828 return added; 829 } 830 831 /** Adds the given {@link CanvasViewInfo} into our internal view list */ add(CanvasViewInfo info)832 private View add(CanvasViewInfo info) { 833 Element element = getElement(info); 834 View view = new View(info, element); 835 mViews.add(view); 836 mElementToView.put(element, view); 837 return view; 838 } 839 analyzeKnownLayouts()840 private void analyzeKnownLayouts() { 841 Set<Element> parents = new HashSet<Element>(); 842 for (View view : mViews) { 843 Node parent = view.getElement().getParentNode(); 844 if (parent instanceof Element) { 845 parents.add((Element) parent); 846 } 847 } 848 849 List<Collection<View>> rowGroups = new ArrayList<Collection<View>>(); 850 List<Collection<View>> columnGroups = new ArrayList<Collection<View>>(); 851 for (Element parent : parents) { 852 String tagName = parent.getTagName(); 853 if (tagName.equals(LINEAR_LAYOUT) || tagName.equals(TABLE_LAYOUT) || 854 tagName.equals(TABLE_ROW) || tagName.equals(RADIO_GROUP)) { 855 Set<View> group = new HashSet<View>(); 856 for (Element child : DomUtilities.getChildren(parent)) { 857 View view = mElementToView.get(child); 858 if (view != null) { 859 group.add(view); 860 } 861 } 862 if (group.size() > 1) { 863 boolean isVertical = VALUE_VERTICAL.equals(parent.getAttributeNS( 864 ANDROID_URI, ATTR_ORIENTATION)); 865 if (tagName.equals(TABLE_LAYOUT)) { 866 isVertical = true; 867 } else if (tagName.equals(TABLE_ROW)) { 868 isVertical = false; 869 } 870 if (isVertical) { 871 columnGroups.add(group); 872 } else { 873 rowGroups.add(group); 874 } 875 } 876 } else if (tagName.equals(RELATIVE_LAYOUT)) { 877 List<Element> children = DomUtilities.getChildren(parent); 878 for (Element child : children) { 879 View view = mElementToView.get(child); 880 if (view == null) { 881 continue; 882 } 883 NamedNodeMap attributes = child.getAttributes(); 884 for (int i = 0, n = attributes.getLength(); i < n; i++) { 885 Attr attr = (Attr) attributes.item(i); 886 String name = attr.getLocalName(); 887 if (name.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)) { 888 boolean alignVertical = 889 name.equals(ATTR_LAYOUT_ALIGN_TOP) || 890 name.equals(ATTR_LAYOUT_ALIGN_BOTTOM) || 891 name.equals(ATTR_LAYOUT_ALIGN_BASELINE); 892 boolean alignHorizontal = 893 name.equals(ATTR_LAYOUT_ALIGN_LEFT) || 894 name.equals(ATTR_LAYOUT_ALIGN_RIGHT); 895 if (!alignVertical && !alignHorizontal) { 896 continue; 897 } 898 String value = attr.getValue(); 899 if (value.startsWith(ID_PREFIX) 900 || value.startsWith(NEW_ID_PREFIX)) { 901 String targetName = BaseLayoutRule.stripIdPrefix(value); 902 Element target = null; 903 for (Element c : children) { 904 String id = VisualRefactoring.getId(c); 905 if (targetName.equals(BaseLayoutRule.stripIdPrefix(id))) { 906 target = c; 907 break; 908 } 909 } 910 View targetView = mElementToView.get(target); 911 if (targetView != null) { 912 List<View> group = new ArrayList<View>(2); 913 group.add(view); 914 group.add(targetView); 915 if (alignHorizontal) { 916 columnGroups.add(group); 917 } else { 918 assert alignVertical; 919 rowGroups.add(group); 920 } 921 } 922 } 923 } 924 } 925 } 926 } else { 927 // TODO: Consider looking for interesting metadata from other layouts 928 } 929 } 930 931 // Assign the same top or left coordinates to the groups to ensure that they 932 // all get positioned in the same row or column 933 for (Collection<View> rowGroup : rowGroups) { 934 // Find the smallest one 935 Iterator<View> iterator = rowGroup.iterator(); 936 int smallest = iterator.next().mY1; 937 while (iterator.hasNext()) { 938 smallest = Math.min(smallest, iterator.next().mY1); 939 } 940 for (View view : rowGroup) { 941 view.mY2 -= (view.mY1 - smallest); 942 view.mY1 = smallest; 943 } 944 } 945 for (Collection<View> columnGroup : columnGroups) { 946 Iterator<View> iterator = columnGroup.iterator(); 947 int smallest = iterator.next().mX1; 948 while (iterator.hasNext()) { 949 smallest = Math.min(smallest, iterator.next().mX1); 950 } 951 for (View view : columnGroup) { 952 view.mX2 -= (view.mX1 - smallest); 953 view.mX1 = smallest; 954 } 955 } 956 } 957 958 /** 959 * Returns true if the given {@link CanvasViewInfo} represents an element we 960 * should remove in a flattening conversion. We don't want to remove non-layout 961 * views, or layout views that for example contain drawables on their own. 962 */ isRemovableLayout(CanvasViewInfo child)963 private boolean isRemovableLayout(CanvasViewInfo child) { 964 // The element being converted is NOT removable! 965 Element element = getElement(child); 966 if (element == mLayout) { 967 return false; 968 } 969 970 ElementDescriptor descriptor = child.getUiViewNode().getDescriptor(); 971 String name = descriptor.getXmlLocalName(); 972 if (name.equals(LINEAR_LAYOUT) || name.equals(RELATIVE_LAYOUT) 973 || name.equals(TABLE_LAYOUT) || name.equals(TABLE_ROW)) { 974 // Don't delete layouts that provide a background image or gradient 975 if (element.hasAttributeNS(ANDROID_URI, ATTR_BACKGROUND)) { 976 AdtPlugin.log(IStatus.WARNING, 977 "Did not flatten layout %1$s because it defines a '%2$s' attribute", 978 VisualRefactoring.getId(element), ATTR_BACKGROUND); 979 return false; 980 } 981 982 return true; 983 } 984 985 return false; 986 } 987 } 988 } 989