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