public void testDropTopEdge() { // If we drag right into the button itself, not a valid drop position INode inserted = dragInto( new Rect(0, 0, 105, 80), new Point(30, -10), null, 2, -1, // Bounds rectangle "useStyle(DROP_RECIPIENT), drawRect(Rect[0,0,240,480])", // Preview line + drop zone rectangle along the top "useStyle(DROP_ZONE), drawRect(Rect[0,-10,240,20])", "useStyle(DROP_ZONE_ACTIVE), fillRect(Rect[0,-10,240,20])", "useStyle(DROP_PREVIEW), drawLine(0,0,240,0)", // Tip "useStyle(HELP), drawBoxedStrings(5,15,[alignParentTop])", // Drop preview "useStyle(DROP_PREVIEW), drawRect(Rect[0,0,105,80])"); assertEquals("true", inserted.getStringAttr(ANDROID_URI, "layout_alignParentTop")); }
@Override public void onCreate(@NonNull INode node, @NonNull INode parent, @NonNull InsertType insertType) { super.onCreate(node, parent, insertType); if (insertType.isCreate()) { for (int i = 0; i < 3; i++) { INode handle = node.appendChild(SdkConstants.FQCN_RADIO_BUTTON); handle.setAttribute(ANDROID_URI, ATTR_ID, String.format("@+id/radio%d", i)); if (i == 0) { handle.setAttribute(ANDROID_URI, ATTR_CHECKED, VALUE_TRUE); } } } }
/** * Called when a drop is completed and we're in grid-editing mode. This will insert the dragged * element into the target cell. * * @param targetNode the GridLayout node * @param element the dragged element * @return the newly created node */ public INode handleGridModeDrop(INode targetNode, IDragElement element) { String fqcn = element.getFqcn(); INode newChild = targetNode.appendChild(fqcn); int column = mColumnMatch.cellIndex; if (mColumnMatch.createCell) { mGrid.addColumn(column, newChild, UNDEFINED, false, UNDEFINED, UNDEFINED); } int row = mRowMatch.cellIndex; if (mRowMatch.createCell) { mGrid.addRow(row, newChild, UNDEFINED, false, UNDEFINED, UNDEFINED); } mGrid.setGridAttribute(newChild, ATTR_LAYOUT_COLUMN, column); mGrid.setGridAttribute(newChild, ATTR_LAYOUT_ROW, row); int gravity = 0; if (mColumnMatch.type == SegmentType.RIGHT) { gravity |= GravityHelper.GRAVITY_RIGHT; } else if (mColumnMatch.type == SegmentType.CENTER_HORIZONTAL) { gravity |= GravityHelper.GRAVITY_CENTER_HORIZ; } if (mRowMatch.type == SegmentType.BASELINE) { // There *is* no baseline gravity constant, instead, leave the // vertical gravity unspecified and GridLayout will treat it as // baseline alignment // gravity |= GravityHelper.GRAVITY_BASELINE; } else if (mRowMatch.type == SegmentType.BOTTOM) { gravity |= GravityHelper.GRAVITY_BOTTOM; } else if (mRowMatch.type == SegmentType.CENTER_VERTICAL) { gravity |= GravityHelper.GRAVITY_CENTER_VERT; } if (!GravityHelper.isConstrainedHorizontally(gravity)) { gravity |= GravityHelper.GRAVITY_LEFT; } if (!GravityHelper.isConstrainedVertically(gravity)) { gravity |= GravityHelper.GRAVITY_TOP; } mGrid.setGridAttribute(newChild, ATTR_LAYOUT_GRAVITY, getGravity(gravity)); if (mGrid.declaredColumnCount == UNDEFINED || mGrid.declaredColumnCount < column + 1) { mGrid.setGridAttribute(mGrid.layout, ATTR_COLUMN_COUNT, column + 1); } return newChild; }
public void testDropZones() { List<Pair<Point, String[]>> zones = new ArrayList<Pair<Point, String[]>>(); zones.add( Pair.of( new Point(51 + 10, 181 + 10), new String[] {"above=@+id/Centered", "toLeftOf=@+id/Centered"})); zones.add( Pair.of( new Point(71 + 10, 181 + 10), new String[] {"above=@+id/Centered", "alignLeft=@+id/Centered"})); zones.add( Pair.of( new Point(104 + 10, 181 + 10), new String[] {"above=@+id/Centered", "alignRight=@+id/Centered"})); zones.add( Pair.of( new Point(137 + 10, 181 + 10), new String[] {"above=@+id/Centered", "alignRight=@+id/Centered"})); zones.add( Pair.of( new Point(170 + 10, 181 + 10), new String[] {"above=@+id/Centered", "toRightOf=@+id/Centered"})); zones.add( Pair.of( new Point(51 + 10, 279 + 10), new String[] {"below=@+id/Centered", "toLeftOf=@+id/Centered"})); zones.add( Pair.of( new Point(71 + 10, 279 + 10), new String[] {"below=@+id/Centered", "alignLeft=@+id/Centered"})); zones.add( Pair.of( new Point(104 + 10, 279 + 10), new String[] {"below=@+id/Centered", "alignLeft=@+id/Centered"})); zones.add( Pair.of( new Point(137 + 10, 279 + 10), new String[] {"below=@+id/Centered", "alignRight=@+id/Centered"})); zones.add( Pair.of( new Point(170 + 10, 279 + 10), new String[] {"below=@+id/Centered", "toRightOf=@+id/Centered"})); zones.add( Pair.of( new Point(51 + 10, 201 + 10), new String[] {"toLeftOf=@+id/Centered", "alignTop=@+id/Centered"})); zones.add( Pair.of( new Point(51 + 10, 227 + 10), new String[] {"toLeftOf=@+id/Centered", "alignTop=@+id/Centered"})); zones.add( Pair.of( new Point(170 + 10, 201 + 10), new String[] {"toRightOf=@+id/Centered", "alignTop=@+id/Centered"})); zones.add( Pair.of( new Point(51 + 10, 253 + 10), new String[] {"toLeftOf=@+id/Centered", "alignBottom=@+id/Centered"})); zones.add( Pair.of( new Point(170 + 10, 227 + 10), new String[] { "toRightOf=@+id/Centered", "alignTop=@+id/Centered", "alignBottom=@+id/Centered" })); zones.add( Pair.of( new Point(170 + 10, 253 + 10), new String[] {"toRightOf=@+id/Centered", "alignBottom=@+id/Centered"})); for (Pair<Point, String[]> zonePair : zones) { Point dropPoint = zonePair.getFirst(); String[] attachments = zonePair.getSecond(); // If we drag right into the button itself, not a valid drop position INode inserted = dragInto( new Rect(0, 0, 105, 80), new Point(120, 240), dropPoint, 1, -1, attachments, // Bounds rectangle "useStyle(DROP_RECIPIENT), drawRect(Rect[0,0,240,480])", // Drop zones "useStyle(DROP_ZONE), " + "drawRect(Rect[51,181,20,20]), drawRect(Rect[71,181,33,20]), " + "drawRect(Rect[104,181,33,20]), drawRect(Rect[137,181,33,20]), " + "drawRect(Rect[170,181,20,20]), drawRect(Rect[51,279,20,20]), " + "drawRect(Rect[71,279,33,20]), drawRect(Rect[104,279,33,20]), " + "drawRect(Rect[137,279,33,20]), drawRect(Rect[170,279,20,20]), " + "drawRect(Rect[51,201,20,26]), drawRect(Rect[51,227,20,26]), " + "drawRect(Rect[51,253,20,26]), drawRect(Rect[170,201,20,26]), " + "drawRect(Rect[170,227,20,26]), drawRect(Rect[170,253,20,26])"); for (String attachment : attachments) { String[] elements = attachment.split("="); String name = "layout_" + elements[0]; String value = elements[1]; assertEquals(value, inserted.getStringAttr(ANDROID_URI, name)); } } }
/** * Called when a node is dropped in free-form mode. This will insert the dragged element into the * grid and returns the newly created node. * * @param targetNode the GridLayout node * @param element the dragged element * @return the newly created {@link INode} */ public INode handleFreeFormDrop(INode targetNode, IDragElement element) { assert mRowMatch != null; assert mColumnMatch != null; String fqcn = element.getFqcn(); INode newChild = null; Rect bounds = element.getBounds(); int row = mRowMatch.cellIndex; int column = mColumnMatch.cellIndex; if (targetNode.getChildren().length == 0) { // // Set up the initial structure: // // // Fixed Fixed // Size Size // Column Expanding Column Column // +-----+-------------------------------+-----+ // | | | | // | 0,0 | 0,1 | 0,2 | Fixed Size Row // | | | | // +-----+-------------------------------+-----+ // | | | | // | | | | // | | | | // | 1,0 | 1,1 | 1,2 | Expanding Row // | | | | // | | | | // | | | | // +-----+-------------------------------+-----+ // | | | | // | 2,0 | 2,1 | 2,2 | Fixed Size Row // | | | | // +-----+-------------------------------+-----+ // // This is implemented in GridLayout by the following grid, where // SC1 has columnWeight=1 and SR1 has rowWeight=1. // (SC=Space for Column, SR=Space for Row) // // +------+-------------------------------+------+ // | | | | // | SCR0 | SC1 | SC2 | // | | | | // +------+-------------------------------+------+ // | | | | // | | | | // | | | | // | SR1 | | | // | | | | // | | | | // | | | | // +------+-------------------------------+------+ // | | | | // | SR2 | | | // | | | | // +------+-------------------------------+------+ // // Note that when we split columns and rows here, if splitting the expanding // row or column then the row or column weight should be moved to the right or // bottom half! // int columnX = mGrid.getColumnX(column); // int rowY = mGrid.getRowY(row); mGrid.setGridAttribute(targetNode, ATTR_COLUMN_COUNT, 2); // mGrid.setGridAttribute(targetNode, ATTR_COLUMN_COUNT, 3); // INode scr0 = addSpacer(targetNode, -1, 0, 0, 1, 1); // INode sc1 = addSpacer(targetNode, -1, 0, 1, 0, 0); // INode sc2 = addSpacer(targetNode, -1, 0, 2, 1, 0); // INode sr1 = addSpacer(targetNode, -1, 1, 0, 0, 0); // INode sr2 = addSpacer(targetNode, -1, 2, 0, 0, 1); // mGrid.setGridAttribute(sc1, ATTR_LAYOUT_GRAVITY, VALUE_FILL_HORIZONTAL); // mGrid.setGridAttribute(sr1, ATTR_LAYOUT_GRAVITY, VALUE_FILL_VERTICAL); // // mGrid.loadFromXml(); // column = mGrid.getColumn(columnX); // row = mGrid.getRow(rowY); } int startX, endX; if (mColumnMatch.type == SegmentType.RIGHT) { endX = mColumnMatch.matchedLine - 1; startX = endX - bounds.w; column = mGrid.getColumn(startX); } else { startX = mColumnMatch.matchedLine; // TODO: What happens on type=RIGHT? endX = startX + bounds.w; } int startY, endY; if (mRowMatch.type == SegmentType.BOTTOM) { endY = mRowMatch.matchedLine - 1; startY = endY - bounds.h; row = mGrid.getRow(startY); } else if (mRowMatch.type == SegmentType.BASELINE) { // TODO: The rowSpan should always be 1 for baseline alignments, since // otherwise the alignment won't work! startY = endY = mRowMatch.matchedLine; } else { startY = mRowMatch.matchedLine; endY = startY + bounds.h; } int endColumn = mGrid.getColumn(endX); int endRow = mGrid.getRow(endY); int columnSpan = endColumn - column + 1; int rowSpan = endRow - row + 1; // Make sure my math was right: assert mRowMatch.type != SegmentType.BASELINE || rowSpan == 1 : rowSpan; // If the item almost fits into the row (at most N % bigger) then just enlarge // the row; don't add a rowspan since that will defeat baseline alignment etc if (!mRowMatch.createCell && bounds.h <= MAX_CELL_DIFFERENCE * mGrid.getRowHeight(mRowMatch.type == SegmentType.BOTTOM ? endRow : row, 1)) { if (mRowMatch.type == SegmentType.BOTTOM) { row += rowSpan - 1; } rowSpan = 1; } if (!mColumnMatch.createCell && bounds.w <= MAX_CELL_DIFFERENCE * mGrid.getColumnWidth( mColumnMatch.type == SegmentType.RIGHT ? endColumn : column, 1)) { if (mColumnMatch.type == SegmentType.RIGHT) { column += columnSpan - 1; } columnSpan = 1; } if (mColumnMatch.type == SegmentType.CENTER_HORIZONTAL) { column = 0; columnSpan = mGrid.actualColumnCount; } // Temporary: Ensure we don't get in trouble with implicit positions mGrid.applyPositionAttributes(); // Split cells to make a new column if (mColumnMatch.createCell) { int columnWidthPx = mGrid.getColumnDistance(column, mColumnMatch.matchedLine); // assert columnWidthPx == columnMatch.distance; // TBD? IF so simplify int columnWidthDp = mRule.getRulesEngine().pxToDp(columnWidthPx); int maxX = mGrid.getColumnMaxX(column); boolean insertMarginColumn = false; if (mColumnMatch.margin == 0) { columnWidthDp = 0; } else if (mColumnMatch.margin != UNDEFINED) { int distance = abs(mColumnMatch.matchedLine - (maxX + mColumnMatch.margin)); insertMarginColumn = column > 0 && distance < 2; if (insertMarginColumn) { int margin = mColumnMatch.margin; if (ViewMetadataRepository.INSETS_SUPPORTED) { IViewMetadata metadata = mRule.getRulesEngine().getMetadata(fqcn); if (metadata != null) { Margins insets = metadata.getInsets(); if (insets != null) { // TODO: // Consider left or right side attachment // TODO: Also consider inset of element on cell to the left margin -= insets.left; } } } columnWidthDp = mRule.getRulesEngine().pxToDp(margin); } } column++; mGrid.splitColumn(column, insertMarginColumn, columnWidthDp, mColumnMatch.matchedLine); if (insertMarginColumn) { column++; } } // Split cells to make a new row if (mRowMatch.createCell) { int rowHeightPx = mGrid.getRowDistance(row, mRowMatch.matchedLine); // assert rowHeightPx == rowMatch.distance; // TBD? If so simplify int rowHeightDp = mRule.getRulesEngine().pxToDp(rowHeightPx); int maxY = mGrid.getRowMaxY(row); boolean insertMarginRow = false; if (mRowMatch.margin == 0) { rowHeightDp = 0; } else if (mRowMatch.margin != UNDEFINED) { int distance = abs(mRowMatch.matchedLine - (maxY + mRowMatch.margin)); insertMarginRow = row > 0 && distance < 2; if (insertMarginRow) { int margin = mRowMatch.margin; IViewMetadata metadata = mRule.getRulesEngine().getMetadata(element.getFqcn()); if (metadata != null) { Margins insets = metadata.getInsets(); if (insets != null) { // TODO: // Consider left or right side attachment // TODO: Also consider inset of element on cell to the left margin -= insets.top; } } rowHeightDp = mRule.getRulesEngine().pxToDp(margin); } } row++; mGrid.splitRow(row, insertMarginRow, rowHeightDp, mRowMatch.matchedLine); if (insertMarginRow) { row++; } } // Figure out where to insert the new child int index = mGrid.getInsertIndex(row, column); if (index == -1) { // Couldn't find a later place to insert newChild = targetNode.appendChild(fqcn); } else { GridModel.ViewData next = mGrid.getView(index); newChild = targetNode.insertChildAt(fqcn, index); // Must also apply positions to the following child to ensure // that the new child doesn't affect the implicit numbering! // TODO: We can later check whether the implied number is equal to // what it already is such that we don't need this next.applyPositionAttributes(); } // Set the cell position (gravity) of the new widget int gravity = 0; if (mColumnMatch.type == SegmentType.RIGHT) { gravity |= GravityHelper.GRAVITY_RIGHT; } else if (mColumnMatch.type == SegmentType.CENTER_HORIZONTAL) { gravity |= GravityHelper.GRAVITY_CENTER_HORIZ; } mGrid.setGridAttribute(newChild, ATTR_LAYOUT_COLUMN, column); if (mRowMatch.type == SegmentType.BASELINE) { // There *is* no baseline gravity constant, instead, leave the // vertical gravity unspecified and GridLayout will treat it as // baseline alignment // gravity |= GravityHelper.GRAVITY_BASELINE; } else if (mRowMatch.type == SegmentType.BOTTOM) { gravity |= GravityHelper.GRAVITY_BOTTOM; } else if (mRowMatch.type == SegmentType.CENTER_VERTICAL) { gravity |= GravityHelper.GRAVITY_CENTER_VERT; } // Ensure that we have at least one horizontal and vertical constraint, otherwise // the new item will be fixed. As an example, if we have a single button in the // table which we inserted *without* a gravity, and we then insert a button // above it with a vertical gravity, then only the top column would be considered // stretchable, and it will fill all available vertical space and the previous // button will jump to the bottom. if (!GravityHelper.isConstrainedHorizontally(gravity)) { gravity |= GravityHelper.GRAVITY_LEFT; } /* This causes problems: Try placing two buttons vertically from the top of the layout. We need to solve the free column/free row problem first. if (!GravityHelper.isConstrainedVertically(gravity) // There is no baseline constant, so we have to leave it unconstrained instead && mRowMatch.type != SegmentType.BASELINE // You also can't baseline align one element with another that has vertical // alignment top or bottom, so when we first "freely" place views (e.g. // at a particular y location), also place it freely (no constraint). && !mRowMatch.createCell) { gravity |= GravityHelper.GRAVITY_TOP; } */ mGrid.setGridAttribute(newChild, ATTR_LAYOUT_GRAVITY, getGravity(gravity)); mGrid.setGridAttribute(newChild, ATTR_LAYOUT_ROW, row); // Apply spans to ensure that the widget can fit without pushing columns if (columnSpan > 1) { mGrid.setGridAttribute(newChild, ATTR_LAYOUT_COLUMN_SPAN, columnSpan); } if (rowSpan > 1) { mGrid.setGridAttribute(newChild, ATTR_LAYOUT_ROW_SPAN, rowSpan); } // Ensure that we don't store columnCount=0 if (mGrid.actualColumnCount == 0) { mGrid.setGridAttribute(mGrid.layout, ATTR_COLUMN_COUNT, Math.max(1, column + 1)); } return newChild; }