| /* |
| * Copyright (C) 2011 The Android Open Source Project |
| * |
| * Licensed under the Eclipse Public License, Version 1.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.eclipse.org/org/documents/epl-v10.php |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.android.ide.common.layout.grid; |
| |
| import static com.android.SdkConstants.ANDROID_URI; |
| import static com.android.SdkConstants.ATTR_COLUMN_COUNT; |
| import static com.android.SdkConstants.ATTR_ID; |
| import static com.android.SdkConstants.ATTR_LAYOUT_COLUMN; |
| import static com.android.SdkConstants.ATTR_LAYOUT_COLUMN_SPAN; |
| import static com.android.SdkConstants.ATTR_LAYOUT_GRAVITY; |
| import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT; |
| import static com.android.SdkConstants.ATTR_LAYOUT_ROW; |
| import static com.android.SdkConstants.ATTR_LAYOUT_ROW_SPAN; |
| import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH; |
| import static com.android.SdkConstants.ATTR_ORIENTATION; |
| import static com.android.SdkConstants.ATTR_ROW_COUNT; |
| import static com.android.SdkConstants.FQCN_GRID_LAYOUT; |
| import static com.android.SdkConstants.FQCN_SPACE; |
| import static com.android.SdkConstants.FQCN_SPACE_V7; |
| import static com.android.SdkConstants.GRID_LAYOUT; |
| import static com.android.SdkConstants.NEW_ID_PREFIX; |
| import static com.android.SdkConstants.SPACE; |
| import static com.android.SdkConstants.VALUE_BOTTOM; |
| import static com.android.SdkConstants.VALUE_CENTER_VERTICAL; |
| import static com.android.SdkConstants.VALUE_N_DP; |
| import static com.android.SdkConstants.VALUE_TOP; |
| import static com.android.SdkConstants.VALUE_VERTICAL; |
| import static com.android.ide.common.layout.GravityHelper.GRAVITY_BOTTOM; |
| import static com.android.ide.common.layout.GravityHelper.GRAVITY_CENTER_HORIZ; |
| import static com.android.ide.common.layout.GravityHelper.GRAVITY_CENTER_VERT; |
| import static com.android.ide.common.layout.GravityHelper.GRAVITY_RIGHT; |
| import static java.lang.Math.abs; |
| import static java.lang.Math.max; |
| import static java.lang.Math.min; |
| |
| import com.android.annotations.NonNull; |
| import com.android.annotations.Nullable; |
| import com.android.ide.common.api.IClientRulesEngine; |
| import com.android.ide.common.api.INode; |
| import com.android.ide.common.api.IViewMetadata; |
| import com.android.ide.common.api.Margins; |
| import com.android.ide.common.api.Rect; |
| import com.android.ide.common.layout.GravityHelper; |
| import com.android.ide.common.layout.GridLayoutRule; |
| import com.android.utils.Pair; |
| import com.google.common.collect.ArrayListMultimap; |
| import com.google.common.collect.Multimap; |
| |
| import java.io.PrintWriter; |
| import java.io.StringWriter; |
| import java.lang.ref.WeakReference; |
| import java.lang.reflect.Field; |
| import java.lang.reflect.Method; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| |
| /** Models a GridLayout */ |
| public class GridModel { |
| /** Marker value used to indicate values (rows, columns, etc) which have not been set */ |
| static final int UNDEFINED = Integer.MIN_VALUE; |
| |
| /** The size of spacers in the dimension that they are not defining */ |
| static final int SPACER_SIZE_DP = 1; |
| |
| /** Attribute value used for {@link #SPACER_SIZE_DP} */ |
| private static final String SPACER_SIZE = String.format(VALUE_N_DP, SPACER_SIZE_DP); |
| |
| /** Width assigned to a newly added column with the Add Column action */ |
| private static final int DEFAULT_CELL_WIDTH = 100; |
| |
| /** Height assigned to a newly added row with the Add Row action */ |
| private static final int DEFAULT_CELL_HEIGHT = 15; |
| |
| /** The GridLayout node, never null */ |
| public final INode layout; |
| |
| /** True if this is a vertical layout, and false if it is horizontal (the default) */ |
| public boolean vertical; |
| |
| /** The declared count of rows (which may be {@link #UNDEFINED} if not specified) */ |
| public int declaredRowCount; |
| |
| /** The declared count of columns (which may be {@link #UNDEFINED} if not specified) */ |
| public int declaredColumnCount; |
| |
| /** The actual count of rows found in the grid */ |
| public int actualRowCount; |
| |
| /** The actual count of columns found in the grid */ |
| public int actualColumnCount; |
| |
| /** |
| * Array of positions (indexed by column) of the left edge of table cells; this |
| * corresponds to the column positions in the grid |
| */ |
| private int[] mLeft; |
| |
| /** |
| * Array of positions (indexed by row) of the top edge of table cells; this |
| * corresponds to the row positions in the grid |
| */ |
| private int[] mTop; |
| |
| /** |
| * Array of positions (indexed by column) of the maximum right hand side bounds of a |
| * node in the given column; this represents the visual edge of a column even when the |
| * actual column is wider |
| */ |
| private int[] mMaxRight; |
| |
| /** |
| * Array of positions (indexed by row) of the maximum bottom bounds of a node in the |
| * given row; this represents the visual edge of a row even when the actual row is |
| * taller |
| */ |
| private int[] mMaxBottom; |
| |
| /** |
| * Array of baselines computed for the rows. This array is populated lazily and should |
| * not be accessed directly; call {@link #getBaseline(int)} instead. |
| */ |
| private int[] mBaselines; |
| |
| /** List of all the view data for the children in this layout */ |
| private List<ViewData> mChildViews; |
| |
| /** The {@link IClientRulesEngine} */ |
| private final IClientRulesEngine mRulesEngine; |
| |
| /** |
| * An actual instance of a GridLayout object that this grid model corresponds to. |
| */ |
| private Object mViewObject; |
| |
| /** The namespace to use for attributes */ |
| private String mNamespace; |
| |
| /** |
| * Constructs a {@link GridModel} for the given layout |
| * |
| * @param rulesEngine the associated rules engine |
| * @param node the GridLayout node |
| * @param viewObject an actual GridLayout instance, or null |
| */ |
| private GridModel(IClientRulesEngine rulesEngine, INode node, Object viewObject) { |
| mRulesEngine = rulesEngine; |
| layout = node; |
| mViewObject = viewObject; |
| loadFromXml(); |
| } |
| |
| // Factory cache for most recent item (used primarily because during paints and drags |
| // the grid model is called repeatedly for the same view object.) |
| private static WeakReference<Object> sCachedViewObject = new WeakReference<Object>(null); |
| private static WeakReference<GridModel> sCachedViewModel; |
| |
| /** |
| * Factory which returns a grid model for the given node. |
| * |
| * @param rulesEngine the associated rules engine |
| * @param node the GridLayout node |
| * @param viewObject an actual GridLayout instance, or null |
| * @return a new model |
| */ |
| @NonNull |
| public static GridModel get( |
| @NonNull IClientRulesEngine rulesEngine, |
| @NonNull INode node, |
| @Nullable Object viewObject) { |
| if (viewObject != null && viewObject == sCachedViewObject.get()) { |
| GridModel model = sCachedViewModel.get(); |
| if (model != null) { |
| return model; |
| } |
| } |
| |
| GridModel model = new GridModel(rulesEngine, node, viewObject); |
| sCachedViewModel = new WeakReference<GridModel>(model); |
| sCachedViewObject = new WeakReference<Object>(viewObject); |
| return model; |
| } |
| |
| /** |
| * Returns the {@link ViewData} for the child at the given index |
| * |
| * @param index the position of the child node whose view we want to look up |
| * @return the corresponding {@link ViewData} |
| */ |
| public ViewData getView(int index) { |
| return mChildViews.get(index); |
| } |
| |
| /** |
| * Returns the {@link ViewData} for the given child node. |
| * |
| * @param node the node for which we want the view info |
| * @return the view info for the node, or null if not found |
| */ |
| public ViewData getView(INode node) { |
| for (ViewData view : mChildViews) { |
| if (view.node == node) { |
| return view; |
| } |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Computes the index (among the children nodes) to insert a new node into which |
| * should be positioned at the given row and column. This will skip over any nodes |
| * that have implicit positions earlier than the given node, and will also ensure that |
| * all nodes are placed before the spacer nodes. |
| * |
| * @param row the target row of the new node |
| * @param column the target column of the new node |
| * @return the insert position to use or -1 if no preference is found |
| */ |
| public int getInsertIndex(int row, int column) { |
| if (vertical) { |
| for (ViewData view : mChildViews) { |
| if (view.column > column || view.column == column && view.row >= row) { |
| return view.index; |
| } |
| } |
| } else { |
| for (ViewData view : mChildViews) { |
| if (view.row > row || view.row == row && view.column >= column) { |
| return view.index; |
| } |
| } |
| } |
| |
| // Place it before the first spacer |
| for (ViewData view : mChildViews) { |
| if (view.isSpacer()) { |
| return view.index; |
| } |
| } |
| |
| return -1; |
| } |
| |
| /** |
| * Returns the baseline of the given row, or -1 if none is found. This looks for views |
| * in the row which have baseline vertical alignment and also define their own |
| * baseline, and returns the first such match. |
| * |
| * @param row the row to look up a baseline for |
| * @return the baseline relative to the row position, or -1 if not defined |
| */ |
| public int getBaseline(int row) { |
| if (row < 0 || row >= mBaselines.length) { |
| return -1; |
| } |
| |
| int baseline = mBaselines[row]; |
| if (baseline == UNDEFINED) { |
| baseline = -1; |
| |
| // TBD: Consider stringing together row information in the view data |
| // so I can quickly identify the views in a given row instead of searching |
| // among all? |
| for (ViewData view : mChildViews) { |
| // We only count baselines for views with rowSpan=1 because |
| // baseline alignment doesn't work for cell spanning views |
| if (view.row == row && view.rowSpan == 1) { |
| baseline = view.node.getBaseline(); |
| if (baseline != -1) { |
| // Even views that do have baselines do not count towards a row |
| // baseline if they have a vertical gravity |
| String gravity = getGridAttribute(view.node, ATTR_LAYOUT_GRAVITY); |
| if (gravity == null |
| || !(gravity.contains(VALUE_TOP) |
| || gravity.contains(VALUE_BOTTOM) |
| || gravity.contains(VALUE_CENTER_VERTICAL))) { |
| // Compute baseline relative to the row, not the view itself |
| baseline += view.node.getBounds().y - getRowY(row); |
| break; |
| } |
| } |
| } |
| } |
| mBaselines[row] = baseline; |
| } |
| |
| return baseline; |
| } |
| |
| /** Applies the row and column values into the XML */ |
| void applyPositionAttributes() { |
| for (ViewData view : mChildViews) { |
| view.applyPositionAttributes(); |
| } |
| |
| // Also fix the columnCount |
| if (getGridAttribute(layout, ATTR_COLUMN_COUNT) != null && |
| declaredColumnCount > actualColumnCount) { |
| setGridAttribute(layout, ATTR_COLUMN_COUNT, actualColumnCount); |
| } |
| } |
| |
| /** |
| * Sets the given GridLayout attribute (rowCount, layout_row, etc) to the |
| * given value. This automatically handles using the right XML namespace |
| * based on whether the GridLayout is the android.widget.GridLayout, or the |
| * support library GridLayout, and whether it's in a library project or not |
| * etc. |
| * |
| * @param node the node to apply the attribute to |
| * @param name the local name of the attribute |
| * @param value the integer value to set the attribute to |
| */ |
| public void setGridAttribute(INode node, String name, int value) { |
| setGridAttribute(node, name, Integer.toString(value)); |
| } |
| |
| /** |
| * Sets the given GridLayout attribute (rowCount, layout_row, etc) to the |
| * given value. This automatically handles using the right XML namespace |
| * based on whether the GridLayout is the android.widget.GridLayout, or the |
| * support library GridLayout, and whether it's in a library project or not |
| * etc. |
| * |
| * @param node the node to apply the attribute to |
| * @param name the local name of the attribute |
| * @param value the string value to set the attribute to, or null to clear |
| * it |
| */ |
| public void setGridAttribute(INode node, String name, String value) { |
| node.setAttribute(getNamespace(), name, value); |
| } |
| |
| /** |
| * Returns the namespace URI to use for GridLayout-specific attributes, such |
| * as columnCount, layout_column, layout_column_span, layout_gravity etc. |
| * |
| * @return the namespace, never null |
| */ |
| public String getNamespace() { |
| if (mNamespace == null) { |
| mNamespace = ANDROID_URI; |
| |
| String fqcn = layout.getFqcn(); |
| if (!fqcn.equals(GRID_LAYOUT) && !fqcn.equals(FQCN_GRID_LAYOUT)) { |
| mNamespace = mRulesEngine.getAppNameSpace(); |
| } |
| } |
| |
| return mNamespace; |
| } |
| |
| /** Removes the given flag from a flag attribute value and returns the result */ |
| static String removeFlag(String flag, String value) { |
| if (value.equals(flag)) { |
| return null; |
| } |
| // Handle spaces between pipes and flag are a prefix, suffix and interior occurrences |
| int index = value.indexOf(flag); |
| if (index != -1) { |
| int pipe = value.lastIndexOf('|', index); |
| int endIndex = index + flag.length(); |
| if (pipe != -1) { |
| value = value.substring(0, pipe).trim() + value.substring(endIndex).trim(); |
| } else { |
| pipe = value.indexOf('|', endIndex); |
| if (pipe != -1) { |
| value = value.substring(0, index).trim() + value.substring(pipe + 1).trim(); |
| } else { |
| value = value.substring(0, index).trim() + value.substring(endIndex).trim(); |
| } |
| } |
| } |
| |
| return value; |
| } |
| |
| /** |
| * Loads a {@link GridModel} from the XML model. |
| */ |
| private void loadFromXml() { |
| INode[] children = layout.getChildren(); |
| |
| declaredRowCount = getGridAttribute(layout, ATTR_ROW_COUNT, UNDEFINED); |
| declaredColumnCount = getGridAttribute(layout, ATTR_COLUMN_COUNT, UNDEFINED); |
| // Horizontal is the default, so if no value is specified it is horizontal. |
| vertical = VALUE_VERTICAL.equals(getGridAttribute(layout, ATTR_ORIENTATION)); |
| |
| mChildViews = new ArrayList<ViewData>(children.length); |
| int index = 0; |
| for (INode child : children) { |
| ViewData view = new ViewData(child, index++); |
| mChildViews.add(view); |
| } |
| |
| // Assign row/column positions to all cells that do not explicitly define them |
| if (!assignRowsAndColumnsFromViews(mChildViews)) { |
| assignRowsAndColumnsFromXml( |
| declaredRowCount == UNDEFINED ? children.length : declaredRowCount, |
| declaredColumnCount == UNDEFINED ? children.length : declaredColumnCount); |
| } |
| |
| assignCellBounds(); |
| |
| for (int i = 0; i <= actualRowCount; i++) { |
| mBaselines[i] = UNDEFINED; |
| } |
| } |
| |
| private Pair<Map<Integer, Integer>, Map<Integer, Integer>> findCellsOutsideDeclaredBounds() { |
| // See if we have any (row,column) pairs that fall outside the declared |
| // bounds; for these we identify the number of unique values and assign these |
| // consecutive values |
| Map<Integer, Integer> extraColumnsMap = null; |
| Map<Integer, Integer> extraRowsMap = null; |
| if (declaredRowCount != UNDEFINED) { |
| Set<Integer> extraRows = null; |
| for (ViewData view : mChildViews) { |
| if (view.row >= declaredRowCount) { |
| if (extraRows == null) { |
| extraRows = new HashSet<Integer>(); |
| } |
| extraRows.add(view.row); |
| } |
| } |
| if (extraRows != null && declaredRowCount != UNDEFINED) { |
| List<Integer> rows = new ArrayList<Integer>(extraRows); |
| Collections.sort(rows); |
| int row = declaredRowCount; |
| extraRowsMap = new HashMap<Integer, Integer>(); |
| for (Integer declared : rows) { |
| extraRowsMap.put(declared, row++); |
| } |
| } |
| } |
| if (declaredColumnCount != UNDEFINED) { |
| Set<Integer> extraColumns = null; |
| for (ViewData view : mChildViews) { |
| if (view.column >= declaredColumnCount) { |
| if (extraColumns == null) { |
| extraColumns = new HashSet<Integer>(); |
| } |
| extraColumns.add(view.column); |
| } |
| } |
| if (extraColumns != null && declaredColumnCount != UNDEFINED) { |
| List<Integer> columns = new ArrayList<Integer>(extraColumns); |
| Collections.sort(columns); |
| int column = declaredColumnCount; |
| extraColumnsMap = new HashMap<Integer, Integer>(); |
| for (Integer declared : columns) { |
| extraColumnsMap.put(declared, column++); |
| } |
| } |
| } |
| |
| return Pair.of(extraRowsMap, extraColumnsMap); |
| } |
| |
| /** |
| * Figure out actual row and column numbers for views that do not specify explicit row |
| * and/or column numbers |
| * TODO: Consolidate with the algorithm in GridLayout to ensure we get the |
| * exact same results! |
| */ |
| private void assignRowsAndColumnsFromXml(int rowCount, int columnCount) { |
| Pair<Map<Integer, Integer>, Map<Integer, Integer>> p = findCellsOutsideDeclaredBounds(); |
| Map<Integer, Integer> extraRowsMap = p.getFirst(); |
| Map<Integer, Integer> extraColumnsMap = p.getSecond(); |
| |
| if (!vertical) { |
| // Horizontal GridLayout: this is the default. Row and column numbers |
| // are assigned by assuming that the children are assigned successive |
| // column numbers until we get to the column count of the grid, at which |
| // point we jump to the next row. If any cell specifies either an explicit |
| // row number of column number, we jump to the next available position. |
| // Note also that if there are any rowspans on the current row, then the |
| // next row we jump to is below the largest such rowspan - in other words, |
| // the algorithm does not fill holes in the middle! |
| |
| // TODO: Ensure that we don't run into trouble if a later element specifies |
| // an earlier number... find out what the layout does in that case! |
| int row = 0; |
| int column = 0; |
| int nextRow = 1; |
| for (ViewData view : mChildViews) { |
| int declaredColumn = view.column; |
| if (declaredColumn != UNDEFINED) { |
| if (declaredColumn >= columnCount) { |
| assert extraColumnsMap != null; |
| declaredColumn = extraColumnsMap.get(declaredColumn); |
| view.column = declaredColumn; |
| } |
| if (declaredColumn < column) { |
| // Must jump to the next row to accommodate the new row |
| assert nextRow > row; |
| //row++; |
| row = nextRow; |
| } |
| column = declaredColumn; |
| } else { |
| view.column = column; |
| } |
| if (view.row != UNDEFINED) { |
| // TODO: Should this adjust the column number too? (If so must |
| // also update view.column since we've already processed the local |
| // column number) |
| row = view.row; |
| } else { |
| view.row = row; |
| } |
| |
| nextRow = Math.max(nextRow, view.row + view.rowSpan); |
| |
| // Advance |
| column += view.columnSpan; |
| if (column >= columnCount) { |
| column = 0; |
| assert nextRow > row; |
| //row++; |
| row = nextRow; |
| } |
| } |
| } else { |
| // Vertical layout: successive children are assigned to the same column in |
| // successive rows. |
| int row = 0; |
| int column = 0; |
| int nextColumn = 1; |
| for (ViewData view : mChildViews) { |
| int declaredRow = view.row; |
| if (declaredRow != UNDEFINED) { |
| if (declaredRow >= rowCount) { |
| declaredRow = extraRowsMap.get(declaredRow); |
| view.row = declaredRow; |
| } |
| if (declaredRow < row) { |
| // Must jump to the next column to accommodate the new column |
| assert nextColumn > column; |
| column = nextColumn; |
| } |
| row = declaredRow; |
| } else { |
| view.row = row; |
| } |
| if (view.column != UNDEFINED) { |
| // TODO: Should this adjust the row number too? (If so must |
| // also update view.row since we've already processed the local |
| // row number) |
| column = view.column; |
| } else { |
| view.column = column; |
| } |
| |
| nextColumn = Math.max(nextColumn, view.column + view.columnSpan); |
| |
| // Advance |
| row += view.rowSpan; |
| if (row >= rowCount) { |
| row = 0; |
| assert nextColumn > column; |
| //row++; |
| column = nextColumn; |
| } |
| } |
| } |
| } |
| |
| private static boolean sAttemptSpecReflection = true; |
| |
| private boolean assignRowsAndColumnsFromViews(List<ViewData> views) { |
| if (!sAttemptSpecReflection) { |
| return false; |
| } |
| |
| try { |
| // Lazily initialized reflection methods |
| Field spanField = null; |
| Field rowSpecField = null; |
| Field colSpecField = null; |
| Field minField = null; |
| Field maxField = null; |
| Method getLayoutParams = null; |
| |
| for (ViewData view : views) { |
| // TODO: If the element *specifies* anything in XML, use that instead |
| Object child = mRulesEngine.getViewObject(view.node); |
| if (child == null) { |
| // Fallback to XML model |
| return false; |
| } |
| |
| if (getLayoutParams == null) { |
| getLayoutParams = child.getClass().getMethod("getLayoutParams"); //$NON-NLS-1$ |
| } |
| Object layoutParams = getLayoutParams.invoke(child); |
| if (rowSpecField == null) { |
| Class<? extends Object> layoutParamsClass = layoutParams.getClass(); |
| rowSpecField = layoutParamsClass.getDeclaredField("rowSpec"); //$NON-NLS-1$ |
| colSpecField = layoutParamsClass.getDeclaredField("columnSpec"); //$NON-NLS-1$ |
| rowSpecField.setAccessible(true); |
| colSpecField.setAccessible(true); |
| } |
| assert colSpecField != null; |
| |
| Object rowSpec = rowSpecField.get(layoutParams); |
| Object colSpec = colSpecField.get(layoutParams); |
| if (spanField == null) { |
| spanField = rowSpec.getClass().getDeclaredField("span"); //$NON-NLS-1$ |
| spanField.setAccessible(true); |
| } |
| assert spanField != null; |
| Object rowInterval = spanField.get(rowSpec); |
| Object colInterval = spanField.get(colSpec); |
| if (minField == null) { |
| Class<? extends Object> intervalClass = rowInterval.getClass(); |
| minField = intervalClass.getDeclaredField("min"); //$NON-NLS-1$ |
| maxField = intervalClass.getDeclaredField("max"); //$NON-NLS-1$ |
| minField.setAccessible(true); |
| maxField.setAccessible(true); |
| } |
| assert maxField != null; |
| |
| int row = minField.getInt(rowInterval); |
| int col = minField.getInt(colInterval); |
| int rowEnd = maxField.getInt(rowInterval); |
| int colEnd = maxField.getInt(colInterval); |
| |
| view.column = col; |
| view.row = row; |
| view.columnSpan = colEnd - col; |
| view.rowSpan = rowEnd - row; |
| } |
| |
| return true; |
| |
| } catch (Throwable e) { |
| sAttemptSpecReflection = false; |
| return false; |
| } |
| } |
| |
| /** |
| * Computes the positions of the column and row boundaries |
| */ |
| private void assignCellBounds() { |
| if (!assignCellBoundsFromView()) { |
| assignCellBoundsFromBounds(); |
| } |
| initializeMaxBounds(); |
| mBaselines = new int[actualRowCount + 1]; |
| } |
| |
| /** |
| * Computes the positions of the column and row boundaries, using actual |
| * layout data from the associated GridLayout instance (stored in |
| * {@link #mViewObject}) |
| */ |
| private boolean assignCellBoundsFromView() { |
| if (mViewObject != null) { |
| Pair<int[], int[]> cellBounds = GridModel.getAxisBounds(mViewObject); |
| if (cellBounds != null) { |
| int[] xs = cellBounds.getFirst(); |
| int[] ys = cellBounds.getSecond(); |
| Rect layoutBounds = layout.getBounds(); |
| |
| // Handle "blank" grid layouts: insert a fake grid of CELL_COUNT^2 cells |
| // where the user can do initial placement |
| if (actualColumnCount <= 1 && actualRowCount <= 1 && mChildViews.isEmpty()) { |
| final int CELL_COUNT = 1; |
| xs = new int[CELL_COUNT + 1]; |
| ys = new int[CELL_COUNT + 1]; |
| int cellWidth = layoutBounds.w / CELL_COUNT; |
| int cellHeight = layoutBounds.h / CELL_COUNT; |
| |
| for (int i = 0; i <= CELL_COUNT; i++) { |
| xs[i] = i * cellWidth; |
| ys[i] = i * cellHeight; |
| } |
| } |
| |
| actualColumnCount = xs.length - 1; |
| actualRowCount = ys.length - 1; |
| |
| int layoutBoundsX = layoutBounds.x; |
| int layoutBoundsY = layoutBounds.y; |
| mLeft = new int[xs.length]; |
| mTop = new int[ys.length]; |
| for (int i = 0; i < xs.length; i++) { |
| mLeft[i] = xs[i] + layoutBoundsX; |
| } |
| for (int i = 0; i < ys.length; i++) { |
| mTop[i] = ys[i] + layoutBoundsY; |
| } |
| |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| /** |
| * Computes the boundaries of the rows and columns by considering the bounds of the |
| * children. |
| */ |
| private void assignCellBoundsFromBounds() { |
| Rect layoutBounds = layout.getBounds(); |
| |
| // Compute the actualColumnCount and actualRowCount. This -should- be |
| // as easy as declaredColumnCount + extraColumnsMap.size(), |
| // but the user doesn't *have* to declare a column count (or a row count) |
| // and we need both, so go and find the actual row and column maximums. |
| int maxColumn = 0; |
| int maxRow = 0; |
| for (ViewData view : mChildViews) { |
| maxColumn = max(maxColumn, view.column); |
| maxRow = max(maxRow, view.row); |
| } |
| actualColumnCount = maxColumn + 1; |
| actualRowCount = maxRow + 1; |
| |
| mLeft = new int[actualColumnCount + 1]; |
| for (int i = 1; i < actualColumnCount; i++) { |
| mLeft[i] = UNDEFINED; |
| } |
| mLeft[0] = layoutBounds.x; |
| mLeft[actualColumnCount] = layoutBounds.x2(); |
| mTop = new int[actualRowCount + 1]; |
| for (int i = 1; i < actualRowCount; i++) { |
| mTop[i] = UNDEFINED; |
| } |
| mTop[0] = layoutBounds.y; |
| mTop[actualRowCount] = layoutBounds.y2(); |
| |
| for (ViewData view : mChildViews) { |
| Rect bounds = view.node.getBounds(); |
| if (!bounds.isValid()) { |
| continue; |
| } |
| int column = view.column; |
| int row = view.row; |
| |
| if (mLeft[column] == UNDEFINED) { |
| mLeft[column] = bounds.x; |
| } else { |
| mLeft[column] = Math.min(bounds.x, mLeft[column]); |
| } |
| if (mTop[row] == UNDEFINED) { |
| mTop[row] = bounds.y; |
| } else { |
| mTop[row] = Math.min(bounds.y, mTop[row]); |
| } |
| } |
| |
| // Ensure that any empty columns/rows have a valid boundary value; for now, |
| for (int i = actualColumnCount - 1; i >= 0; i--) { |
| if (mLeft[i] == UNDEFINED) { |
| if (i == 0) { |
| mLeft[i] = layoutBounds.x; |
| } else if (i < actualColumnCount - 1) { |
| mLeft[i] = mLeft[i + 1] - 1; |
| if (mLeft[i - 1] != UNDEFINED && mLeft[i] < mLeft[i - 1]) { |
| mLeft[i] = mLeft[i - 1]; |
| } |
| } else { |
| mLeft[i] = layoutBounds.x2(); |
| } |
| } |
| } |
| for (int i = actualRowCount - 1; i >= 0; i--) { |
| if (mTop[i] == UNDEFINED) { |
| if (i == 0) { |
| mTop[i] = layoutBounds.y; |
| } else if (i < actualRowCount - 1) { |
| mTop[i] = mTop[i + 1] - 1; |
| if (mTop[i - 1] != UNDEFINED && mTop[i] < mTop[i - 1]) { |
| mTop[i] = mTop[i - 1]; |
| } |
| } else { |
| mTop[i] = layoutBounds.y2(); |
| } |
| } |
| } |
| |
| // The bounds should be in ascending order now |
| if (false && GridLayoutRule.sDebugGridLayout) { |
| for (int i = 1; i < actualRowCount; i++) { |
| assert mTop[i + 1] >= mTop[i]; |
| } |
| for (int i = 0; i < actualColumnCount; i++) { |
| assert mLeft[i + 1] >= mLeft[i]; |
| } |
| } |
| } |
| |
| /** |
| * Determine, for each row and column, what the largest x and y edges are |
| * within that row or column. This is used to find a natural split point to |
| * suggest when adding something "to the right of" or "below" another view. |
| */ |
| private void initializeMaxBounds() { |
| mMaxRight = new int[actualColumnCount + 1]; |
| mMaxBottom = new int[actualRowCount + 1]; |
| |
| for (ViewData view : mChildViews) { |
| Rect bounds = view.node.getBounds(); |
| if (!bounds.isValid()) { |
| continue; |
| } |
| |
| if (!view.isSpacer()) { |
| int x2 = bounds.x2(); |
| int y2 = bounds.y2(); |
| int column = view.column; |
| int row = view.row; |
| int targetColumn = min(actualColumnCount - 1, |
| column + view.columnSpan - 1); |
| int targetRow = min(actualRowCount - 1, row + view.rowSpan - 1); |
| IViewMetadata metadata = mRulesEngine.getMetadata(view.node.getFqcn()); |
| if (metadata != null) { |
| Margins insets = metadata.getInsets(); |
| if (insets != null) { |
| x2 -= insets.right; |
| y2 -= insets.bottom; |
| } |
| } |
| if (mMaxRight[targetColumn] < x2 |
| && ((view.gravity & (GRAVITY_CENTER_HORIZ | GRAVITY_RIGHT)) == 0)) { |
| mMaxRight[targetColumn] = x2; |
| } |
| if (mMaxBottom[targetRow] < y2 |
| && ((view.gravity & (GRAVITY_CENTER_VERT | GRAVITY_BOTTOM)) == 0)) { |
| mMaxBottom[targetRow] = y2; |
| } |
| } |
| } |
| } |
| |
| /** |
| * Looks up the x[] and y[] locations of the columns and rows in the given GridLayout |
| * instance. |
| * |
| * @param view the GridLayout object, which should already have performed layout |
| * @return a pair of x[] and y[] integer arrays, or null if it could not be found |
| */ |
| public static Pair<int[], int[]> getAxisBounds(Object view) { |
| try { |
| Class<?> clz = view.getClass(); |
| String verticalAxisName = "verticalAxis"; |
| Field horizontalAxis; |
| try { |
| horizontalAxis = clz.getDeclaredField("horizontalAxis"); //$NON-NLS-1$ |
| } catch (NoSuchFieldException e) { |
| // Field names changed in KitKat |
| horizontalAxis = clz.getDeclaredField("mHorizontalAxis"); //$NON-NLS-1$ |
| verticalAxisName = "mVerticalAxis"; |
| } |
| Field verticalAxis = clz.getDeclaredField(verticalAxisName); |
| horizontalAxis.setAccessible(true); |
| verticalAxis.setAccessible(true); |
| Object horizontal = horizontalAxis.get(view); |
| Object vertical = verticalAxis.get(view); |
| Field locations = horizontal.getClass().getDeclaredField("locations"); //$NON-NLS-1$ |
| assert locations.getType().isArray() : locations.getType(); |
| locations.setAccessible(true); |
| Object horizontalLocations = locations.get(horizontal); |
| Object verticalLocations = locations.get(vertical); |
| int[] xs = (int[]) horizontalLocations; |
| int[] ys = (int[]) verticalLocations; |
| return Pair.of(xs, ys); |
| } catch (Throwable t) { |
| // Probably trying to show a GridLayout on a platform that does not support it. |
| // Return null to indicate that the grid bounds must be computed from view bounds. |
| return null; |
| } |
| } |
| |
| /** |
| * Add a new column. |
| * |
| * @param selectedChildren if null or empty, add the column at the end of the grid, |
| * and otherwise add it before the column of the first selected child |
| * @return the newly added column spacer |
| */ |
| public INode addColumn(List<? extends INode> selectedChildren) { |
| // Determine insert index |
| int newColumn = actualColumnCount; |
| if (selectedChildren != null && selectedChildren.size() > 0) { |
| INode first = selectedChildren.get(0); |
| ViewData view = getView(first); |
| newColumn = view.column; |
| } |
| |
| INode newView = addColumn(newColumn, null, UNDEFINED, false, UNDEFINED, UNDEFINED); |
| if (newView != null) { |
| mRulesEngine.select(Collections.singletonList(newView)); |
| } |
| |
| return newView; |
| } |
| |
| /** |
| * Adds a new column. |
| * |
| * @param newColumn the column index to insert before |
| * @param newView the {@link INode} to insert as the column spacer, which may be null |
| * (in which case a spacer is automatically created) |
| * @param columnWidthDp the width, in device independent pixels, of the column to be |
| * added (which may be {@link #UNDEFINED} |
| * @param split if true, split the existing column into two at the given x position |
| * @param row the row to add the newView to |
| * @param x the x position of the column we're inserting |
| * @return the column spacer |
| */ |
| public INode addColumn(int newColumn, INode newView, int columnWidthDp, |
| boolean split, int row, int x) { |
| // Insert a new column |
| actualColumnCount++; |
| if (declaredColumnCount != UNDEFINED) { |
| declaredColumnCount++; |
| setGridAttribute(layout, ATTR_COLUMN_COUNT, declaredColumnCount); |
| } |
| |
| boolean isLastColumn = true; |
| for (ViewData view : mChildViews) { |
| if (view.column >= newColumn) { |
| isLastColumn = false; |
| break; |
| } |
| } |
| |
| for (ViewData view : mChildViews) { |
| boolean columnSpanSet = false; |
| |
| int endColumn = view.column + view.columnSpan; |
| if (view.column >= newColumn || endColumn == newColumn) { |
| if (view.column == newColumn || endColumn == newColumn) { |
| //if (view.row == 0) { |
| if (newView == null && !isLastColumn) { |
| // Insert a new spacer |
| int index = getChildIndex(layout.getChildren(), view.node); |
| assert view.index == index; // TODO: Get rid of getter |
| if (endColumn == newColumn) { |
| // This cell -ends- at the desired position: insert it after |
| index++; |
| } |
| |
| ViewData newViewData = addSpacer(layout, index, |
| split ? row : UNDEFINED, |
| split ? newColumn - 1 : UNDEFINED, |
| columnWidthDp != UNDEFINED ? columnWidthDp : DEFAULT_CELL_WIDTH, |
| DEFAULT_CELL_HEIGHT); |
| newViewData.column = newColumn - 1; |
| newViewData.row = row; |
| newView = newViewData.node; |
| } |
| |
| // Set the actual row number on the first cell on the new row. |
| // This means we don't really need the spacer above to imply |
| // the new row number, but we use the spacer to assign the row |
| // some height. |
| if (view.column == newColumn) { |
| view.column++; |
| setGridAttribute(view.node, ATTR_LAYOUT_COLUMN, view.column); |
| } // else: endColumn == newColumn: handled below |
| } else if (getGridAttribute(view.node, ATTR_LAYOUT_COLUMN) != null) { |
| view.column++; |
| setGridAttribute(view.node, ATTR_LAYOUT_COLUMN, view.column); |
| } |
| } else if (endColumn > newColumn) { |
| view.columnSpan++; |
| setColumnSpanAttribute(view.node, view.columnSpan); |
| columnSpanSet = true; |
| } |
| |
| if (split && !columnSpanSet && view.node.getBounds().x2() > x) { |
| if (view.node.getBounds().x < x) { |
| view.columnSpan++; |
| setColumnSpanAttribute(view.node, view.columnSpan); |
| } |
| } |
| } |
| |
| // Hardcode the row numbers if the last column is a new column such that |
| // they don't jump back to backfill the previous row's new last cell |
| if (isLastColumn) { |
| for (ViewData view : mChildViews) { |
| if (view.column == 0 && view.row > 0) { |
| setGridAttribute(view.node, ATTR_LAYOUT_ROW, view.row); |
| } |
| } |
| if (split) { |
| assert newView == null; |
| addSpacer(layout, -1, row, newColumn -1, |
| columnWidthDp != UNDEFINED ? columnWidthDp : DEFAULT_CELL_WIDTH, |
| SPACER_SIZE_DP); |
| } |
| } |
| |
| return newView; |
| } |
| |
| /** |
| * Removes the columns containing the given selection |
| * |
| * @param selectedChildren a list of nodes whose columns should be deleted |
| */ |
| public void removeColumns(List<? extends INode> selectedChildren) { |
| if (selectedChildren.size() == 0) { |
| return; |
| } |
| |
| // Figure out which columns should be removed |
| Set<Integer> removeColumns = new HashSet<Integer>(); |
| Set<ViewData> removedViews = new HashSet<ViewData>(); |
| for (INode child : selectedChildren) { |
| ViewData view = getView(child); |
| removedViews.add(view); |
| removeColumns.add(view.column); |
| } |
| // Sort them in descending order such that we can process each |
| // deletion independently |
| List<Integer> removed = new ArrayList<Integer>(removeColumns); |
| Collections.sort(removed, Collections.reverseOrder()); |
| |
| for (int removedColumn : removed) { |
| // Remove column. |
| // First, adjust column count. |
| // TODO: Don't do this if the column being deleted is outside |
| // the declared column range! |
| // TODO: Do this under a write lock? / editXml lock? |
| actualColumnCount--; |
| if (declaredColumnCount != UNDEFINED) { |
| declaredColumnCount--; |
| } |
| |
| // Remove any elements that begin in the deleted columns... |
| // If they have colspan > 1, then we must insert a spacer instead. |
| // For any other elements that overlap, we need to subtract from the span. |
| |
| for (ViewData view : mChildViews) { |
| if (view.column == removedColumn) { |
| int index = getChildIndex(layout.getChildren(), view.node); |
| assert view.index == index; // TODO: Get rid of getter |
| if (view.columnSpan > 1) { |
| // Make a new spacer which is the width of the following |
| // columns |
| int columnWidth = getColumnWidth(removedColumn, view.columnSpan) - |
| getColumnWidth(removedColumn, 1); |
| int columnWidthDip = mRulesEngine.pxToDp(columnWidth); |
| ViewData spacer = addSpacer(layout, index, UNDEFINED, UNDEFINED, |
| columnWidthDip, SPACER_SIZE_DP); |
| spacer.row = 0; |
| spacer.column = removedColumn; |
| } |
| layout.removeChild(view.node); |
| } else if (view.column < removedColumn |
| && view.column + view.columnSpan > removedColumn) { |
| // Subtract column span to skip this item |
| view.columnSpan--; |
| setColumnSpanAttribute(view.node, view.columnSpan); |
| } else if (view.column > removedColumn) { |
| view.column--; |
| if (getGridAttribute(view.node, ATTR_LAYOUT_COLUMN) != null) { |
| setGridAttribute(view.node, ATTR_LAYOUT_COLUMN, view.column); |
| } |
| } |
| } |
| } |
| |
| // Remove children from child list! |
| if (removedViews.size() <= 2) { |
| mChildViews.removeAll(removedViews); |
| } else { |
| List<ViewData> remaining = |
| new ArrayList<ViewData>(mChildViews.size() - removedViews.size()); |
| for (ViewData view : mChildViews) { |
| if (!removedViews.contains(view)) { |
| remaining.add(view); |
| } |
| } |
| mChildViews = remaining; |
| } |
| |
| //if (declaredColumnCount != UNDEFINED) { |
| setGridAttribute(layout, ATTR_COLUMN_COUNT, actualColumnCount); |
| //} |
| |
| } |
| |
| /** |
| * Add a new row. |
| * |
| * @param selectedChildren if null or empty, add the row at the bottom of the grid, |
| * and otherwise add it before the row of the first selected child |
| * @return the newly added row spacer |
| */ |
| public INode addRow(List<? extends INode> selectedChildren) { |
| // Determine insert index |
| int newRow = actualRowCount; |
| if (selectedChildren.size() > 0) { |
| INode first = selectedChildren.get(0); |
| ViewData view = getView(first); |
| newRow = view.row; |
| } |
| |
| INode newView = addRow(newRow, null, UNDEFINED, false, UNDEFINED, UNDEFINED); |
| if (newView != null) { |
| mRulesEngine.select(Collections.singletonList(newView)); |
| } |
| |
| return newView; |
| } |
| |
| /** |
| * Adds a new column. |
| * |
| * @param newRow the row index to insert before |
| * @param newView the {@link INode} to insert as the row spacer, which may be null (in |
| * which case a spacer is automatically created) |
| * @param rowHeightDp the height, in device independent pixels, of the row to be added |
| * (which may be {@link #UNDEFINED} |
| * @param split if true, split the existing row into two at the given y position |
| * @param column the column to add the newView to |
| * @param y the y position of the row we're inserting |
| * @return the row spacer |
| */ |
| public INode addRow(int newRow, INode newView, int rowHeightDp, boolean split, |
| int column, int y) { |
| actualRowCount++; |
| if (declaredRowCount != UNDEFINED) { |
| declaredRowCount++; |
| setGridAttribute(layout, ATTR_ROW_COUNT, declaredRowCount); |
| } |
| |
| boolean added = false; |
| for (ViewData view : mChildViews) { |
| if (view.row >= newRow) { |
| // Adjust the column count |
| if (view.row == newRow && view.column == 0) { |
| // Insert a new spacer |
| if (newView == null) { |
| int index = getChildIndex(layout.getChildren(), view.node); |
| assert view.index == index; // TODO: Get rid of getter |
| if (declaredColumnCount != UNDEFINED && !split) { |
| setGridAttribute(layout, ATTR_COLUMN_COUNT, declaredColumnCount); |
| } |
| ViewData newViewData = addSpacer(layout, index, |
| split ? newRow - 1 : UNDEFINED, |
| split ? column : UNDEFINED, |
| SPACER_SIZE_DP, |
| rowHeightDp != UNDEFINED ? rowHeightDp : DEFAULT_CELL_HEIGHT); |
| newViewData.column = column; |
| newViewData.row = newRow - 1; |
| newView = newViewData.node; |
| } |
| |
| // Set the actual row number on the first cell on the new row. |
| // This means we don't really need the spacer above to imply |
| // the new row number, but we use the spacer to assign the row |
| // some height. |
| view.row++; |
| setGridAttribute(view.node, ATTR_LAYOUT_ROW, view.row); |
| |
| added = true; |
| } else if (getGridAttribute(view.node, ATTR_LAYOUT_ROW) != null) { |
| view.row++; |
| setGridAttribute(view.node, ATTR_LAYOUT_ROW, view.row); |
| } |
| } else { |
| int endRow = view.row + view.rowSpan; |
| if (endRow > newRow) { |
| view.rowSpan++; |
| setRowSpanAttribute(view.node, view.rowSpan); |
| } else if (split && view.node.getBounds().y2() > y) { |
| if (view.node.getBounds().y < y) { |
| view.rowSpan++; |
| setRowSpanAttribute(view.node, view.rowSpan); |
| } |
| } |
| } |
| } |
| |
| if (!added) { |
| // Append a row at the end |
| if (newView == null) { |
| ViewData newViewData = addSpacer(layout, -1, UNDEFINED, UNDEFINED, |
| SPACER_SIZE_DP, |
| rowHeightDp != UNDEFINED ? rowHeightDp : DEFAULT_CELL_HEIGHT); |
| newViewData.column = column; |
| // TODO: MAke sure this row number is right! |
| newViewData.row = split ? newRow - 1 : newRow; |
| newView = newViewData.node; |
| } |
| if (declaredColumnCount != UNDEFINED && !split) { |
| setGridAttribute(layout, ATTR_COLUMN_COUNT, declaredColumnCount); |
| } |
| if (split) { |
| setGridAttribute(newView, ATTR_LAYOUT_ROW, newRow - 1); |
| setGridAttribute(newView, ATTR_LAYOUT_COLUMN, column); |
| } |
| } |
| |
| return newView; |
| } |
| |
| /** |
| * Removes the rows containing the given selection |
| * |
| * @param selectedChildren a list of nodes whose rows should be deleted |
| */ |
| public void removeRows(List<? extends INode> selectedChildren) { |
| if (selectedChildren.size() == 0) { |
| return; |
| } |
| |
| // Figure out which rows should be removed |
| Set<ViewData> removedViews = new HashSet<ViewData>(); |
| Set<Integer> removedRows = new HashSet<Integer>(); |
| for (INode child : selectedChildren) { |
| ViewData view = getView(child); |
| removedViews.add(view); |
| removedRows.add(view.row); |
| } |
| // Sort them in descending order such that we can process each |
| // deletion independently |
| List<Integer> removed = new ArrayList<Integer>(removedRows); |
| Collections.sort(removed, Collections.reverseOrder()); |
| |
| for (int removedRow : removed) { |
| // Remove row. |
| // First, adjust row count. |
| // TODO: Don't do this if the row being deleted is outside |
| // the declared row range! |
| actualRowCount--; |
| if (declaredRowCount != UNDEFINED) { |
| declaredRowCount--; |
| setGridAttribute(layout, ATTR_ROW_COUNT, declaredRowCount); |
| } |
| |
| // Remove any elements that begin in the deleted rows... |
| // If they have colspan > 1, then we must hardcode a new row number |
| // instead. |
| // For any other elements that overlap, we need to subtract from the span. |
| |
| for (ViewData view : mChildViews) { |
| if (view.row == removedRow) { |
| // We don't have to worry about a rowSpan > 1 here, because even |
| // if it is, those rowspans are not used to assign default row/column |
| // positions for other cells |
| // TODO: Check this; it differs from the removeColumns logic! |
| layout.removeChild(view.node); |
| } else if (view.row > removedRow) { |
| view.row--; |
| if (getGridAttribute(view.node, ATTR_LAYOUT_ROW) != null) { |
| setGridAttribute(view.node, ATTR_LAYOUT_ROW, view.row); |
| } |
| } else if (view.row < removedRow |
| && view.row + view.rowSpan > removedRow) { |
| // Subtract row span to skip this item |
| view.rowSpan--; |
| setRowSpanAttribute(view.node, view.rowSpan); |
| } |
| } |
| } |
| |
| // Remove children from child list! |
| if (removedViews.size() <= 2) { |
| mChildViews.removeAll(removedViews); |
| } else { |
| List<ViewData> remaining = |
| new ArrayList<ViewData>(mChildViews.size() - removedViews.size()); |
| for (ViewData view : mChildViews) { |
| if (!removedViews.contains(view)) { |
| remaining.add(view); |
| } |
| } |
| mChildViews = remaining; |
| } |
| } |
| |
| /** |
| * Returns the row containing the given y line |
| * |
| * @param y the vertical position |
| * @return the row containing the given line |
| */ |
| public int getRow(int y) { |
| int row = Arrays.binarySearch(mTop, y); |
| if (row == -1) { |
| // Smaller than the first element; just use the first row |
| return 0; |
| } else if (row < 0) { |
| row = -(row + 2); |
| } |
| |
| return row; |
| } |
| |
| /** |
| * Returns the column containing the given x line |
| * |
| * @param x the horizontal position |
| * @return the column containing the given line |
| */ |
| public int getColumn(int x) { |
| int column = Arrays.binarySearch(mLeft, x); |
| if (column == -1) { |
| // Smaller than the first element; just use the first column |
| return 0; |
| } else if (column < 0) { |
| column = -(column + 2); |
| } |
| |
| return column; |
| } |
| |
| /** |
| * Returns the closest row to the given y line. This is |
| * either the row containing the line, or the row below it. |
| * |
| * @param y the vertical position |
| * @return the closest row |
| */ |
| public int getClosestRow(int y) { |
| int row = Arrays.binarySearch(mTop, y); |
| if (row == -1) { |
| // Smaller than the first element; just use the first column |
| return 0; |
| } else if (row < 0) { |
| row = -(row + 2); |
| } |
| |
| if (getRowDistance(row, y) < getRowDistance(row + 1, y)) { |
| return row; |
| } else { |
| return row + 1; |
| } |
| } |
| |
| /** |
| * Returns the closest column to the given x line. This is |
| * either the column containing the line, or the column following it. |
| * |
| * @param x the horizontal position |
| * @return the closest column |
| */ |
| public int getClosestColumn(int x) { |
| int column = Arrays.binarySearch(mLeft, x); |
| if (column == -1) { |
| // Smaller than the first element; just use the first column |
| return 0; |
| } else if (column < 0) { |
| column = -(column + 2); |
| } |
| |
| if (getColumnDistance(column, x) < getColumnDistance(column + 1, x)) { |
| return column; |
| } else { |
| return column + 1; |
| } |
| } |
| |
| /** |
| * Returns the distance between the given x position and the beginning of the given column |
| * |
| * @param column the column |
| * @param x the x position |
| * @return the distance between the two |
| */ |
| public int getColumnDistance(int column, int x) { |
| return abs(getColumnX(column) - x); |
| } |
| |
| /** |
| * Returns the actual width of the given column. This returns the difference between |
| * the rightmost edge of the views (not including spacers) and the left edge of the |
| * column. |
| * |
| * @param column the column |
| * @return the actual width of the non-spacer views in the column |
| */ |
| public int getColumnActualWidth(int column) { |
| return getColumnMaxX(column) - getColumnX(column); |
| } |
| |
| /** |
| * Returns the distance between the given y position and the top of the given row |
| * |
| * @param row the row |
| * @param y the y position |
| * @return the distance between the two |
| */ |
| public int getRowDistance(int row, int y) { |
| return abs(getRowY(row) - y); |
| } |
| |
| /** |
| * Returns the y position of the top of the given row |
| * |
| * @param row the target row |
| * @return the y position of its top edge |
| */ |
| public int getRowY(int row) { |
| return mTop[min(mTop.length - 1, max(0, row))]; |
| } |
| |
| /** |
| * Returns the bottom-most edge of any of the non-spacer children in the given row |
| * |
| * @param row the target row |
| * @return the bottom-most edge of any of the non-spacer children in the row |
| */ |
| public int getRowMaxY(int row) { |
| return mMaxBottom[min(mMaxBottom.length - 1, max(0, row))]; |
| } |
| |
| /** |
| * Returns the actual height of the given row. This returns the difference between |
| * the bottom-most edge of the views (not including spacers) and the top edge of the |
| * row. |
| * |
| * @param row the row |
| * @return the actual height of the non-spacer views in the row |
| */ |
| public int getRowActualHeight(int row) { |
| return getRowMaxY(row) - getRowY(row); |
| } |
| |
| /** |
| * Returns a list of all the nodes that intersects the rows in the range |
| * {@code y1 <= y <= y2}. |
| * |
| * @param y1 the starting y, inclusive |
| * @param y2 the ending y, inclusive |
| * @return a list of nodes intersecting the given rows, never null but possibly empty |
| */ |
| public Collection<INode> getIntersectsRow(int y1, int y2) { |
| List<INode> nodes = new ArrayList<INode>(); |
| |
| for (ViewData view : mChildViews) { |
| if (!view.isSpacer()) { |
| Rect bounds = view.node.getBounds(); |
| if (bounds.y2() >= y1 && bounds.y <= y2) { |
| nodes.add(view.node); |
| } |
| } |
| } |
| |
| return nodes; |
| } |
| |
| /** |
| * Returns the height of the given row or rows (if the rowSpan is greater than 1) |
| * |
| * @param row the target row |
| * @param rowSpan the row span |
| * @return the height in pixels of the given rows |
| */ |
| public int getRowHeight(int row, int rowSpan) { |
| return getRowY(row + rowSpan) - getRowY(row); |
| } |
| |
| /** |
| * Returns the x position of the left edge of the given column |
| * |
| * @param column the target column |
| * @return the x position of its left edge |
| */ |
| public int getColumnX(int column) { |
| return mLeft[min(mLeft.length - 1, max(0, column))]; |
| } |
| |
| /** |
| * Returns the rightmost edge of any of the non-spacer children in the given row |
| * |
| * @param column the target column |
| * @return the rightmost edge of any of the non-spacer children in the column |
| */ |
| public int getColumnMaxX(int column) { |
| return mMaxRight[min(mMaxRight.length - 1, max(0, column))]; |
| } |
| |
| /** |
| * Returns the width of the given column or columns (if the columnSpan is greater than 1) |
| * |
| * @param column the target column |
| * @param columnSpan the column span |
| * @return the width in pixels of the given columns |
| */ |
| public int getColumnWidth(int column, int columnSpan) { |
| return getColumnX(column + columnSpan) - getColumnX(column); |
| } |
| |
| /** |
| * Returns the bounds of the cell at the given row and column position, with the given |
| * row and column spans. |
| * |
| * @param row the target row |
| * @param column the target column |
| * @param rowSpan the row span |
| * @param columnSpan the column span |
| * @return the bounds, in pixels, of the given cell |
| */ |
| public Rect getCellBounds(int row, int column, int rowSpan, int columnSpan) { |
| return new Rect(getColumnX(column), getRowY(row), |
| getColumnWidth(column, columnSpan), |
| getRowHeight(row, rowSpan)); |
| } |
| |
| /** |
| * Produces a display of view contents along with the pixel positions of each |
| * row/column, like the following (used for diagnostics only) |
| * |
| * <pre> |
| * |0 |49 |143 |192 |240 |
| * 36| | |button2 | |
| * 72| |radioButton1 |button2 | |
| * 74|button1 |radioButton1 |button2 | |
| * 108|button1 | |button2 | |
| * 110| | |button2 | |
| * 149| | | | |
| * 320 |
| * </pre> |
| */ |
| @Override |
| public String toString() { |
| // Dump out the view table |
| int cellWidth = 25; |
| |
| List<List<List<ViewData>>> rowList = new ArrayList<List<List<ViewData>>>(mTop.length); |
| for (int row = 0; row < mTop.length; row++) { |
| List<List<ViewData>> columnList = new ArrayList<List<ViewData>>(mLeft.length); |
| for (int col = 0; col < mLeft.length; col++) { |
| columnList.add(new ArrayList<ViewData>(4)); |
| } |
| rowList.add(columnList); |
| } |
| for (ViewData view : mChildViews) { |
| for (int i = 0; i < view.rowSpan; i++) { |
| if (view.row + i > mTop.length) { // Guard against bogus span values |
| break; |
| } |
| if (rowList.size() <= view.row + i) { |
| break; |
| } |
| for (int j = 0; j < view.columnSpan; j++) { |
| List<List<ViewData>> columnList = rowList.get(view.row + i); |
| if (columnList.size() <= view.column + j) { |
| break; |
| } |
| columnList.get(view.column + j).add(view); |
| } |
| } |
| } |
| |
| StringWriter stringWriter = new StringWriter(); |
| PrintWriter out = new PrintWriter(stringWriter); |
| out.printf("%" + cellWidth + "s", ""); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ |
| for (int col = 0; col < actualColumnCount + 1; col++) { |
| out.printf("|%-" + (cellWidth - 1) + "d", mLeft[col]); //$NON-NLS-1$ //$NON-NLS-2$ |
| } |
| out.printf("\n"); //$NON-NLS-1$ |
| for (int row = 0; row < actualRowCount + 1; row++) { |
| out.printf("%" + cellWidth + "d", mTop[row]); //$NON-NLS-1$ //$NON-NLS-2$ |
| if (row == actualRowCount) { |
| break; |
| } |
| for (int col = 0; col < actualColumnCount; col++) { |
| List<ViewData> views = rowList.get(row).get(col); |
| |
| StringBuilder sb = new StringBuilder(); |
| for (ViewData view : views) { |
| String id = view != null ? view.getId() : ""; //$NON-NLS-1$ |
| if (id.startsWith(NEW_ID_PREFIX)) { |
| id = id.substring(NEW_ID_PREFIX.length()); |
| } |
| if (id.length() > cellWidth - 2) { |
| id = id.substring(0, cellWidth - 2); |
| } |
| if (sb.length() > 0) { |
| sb.append(','); |
| } |
| sb.append(id); |
| } |
| String cellString = sb.toString(); |
| if (cellString.contains(",") && cellString.length() > cellWidth - 2) { //$NON-NLS-1$ |
| cellString = cellString.substring(0, cellWidth - 6) + "...,"; //$NON-NLS-1$ |
| } |
| out.printf("|%-" + (cellWidth - 2) + "s ", cellString); //$NON-NLS-1$ //$NON-NLS-2$ |
| } |
| out.printf("\n"); //$NON-NLS-1$ |
| } |
| |
| out.flush(); |
| return stringWriter.toString(); |
| } |
| |
| /** |
| * Split a cell into two or three columns. |
| * |
| * @param newColumn The column number to insert before |
| * @param insertMarginColumn If false, then the cell at newColumn -1 is split with the |
| * left part taking up exactly columnWidthDp dips. If true, then the column |
| * is split twice; the left part is the implicit width of the column, the |
| * new middle (margin) column is exactly the columnWidthDp size and the |
| * right column is the remaining space of the old cell. |
| * @param columnWidthDp The width of the column inserted before the new column (or if |
| * insertMarginColumn is false, then the width of the margin column) |
| * @param x the x coordinate of the new column |
| */ |
| public void splitColumn(int newColumn, boolean insertMarginColumn, int columnWidthDp, int x) { |
| actualColumnCount++; |
| |
| // Insert a new column |
| if (declaredColumnCount != UNDEFINED) { |
| declaredColumnCount++; |
| if (insertMarginColumn) { |
| declaredColumnCount++; |
| } |
| setGridAttribute(layout, ATTR_COLUMN_COUNT, declaredColumnCount); |
| } |
| |
| // Are we inserting a new last column in the grid? That requires some special handling... |
| boolean isLastColumn = true; |
| for (ViewData view : mChildViews) { |
| if (view.column >= newColumn) { |
| isLastColumn = false; |
| break; |
| } |
| } |
| |
| // Hardcode the row numbers if the last column is a new column such that |
| // they don't jump back to backfill the previous row's new last cell: |
| // TODO: Only do this for horizontal layouts! |
| if (isLastColumn) { |
| for (ViewData view : mChildViews) { |
| if (view.column == 0 && view.row > 0) { |
| if (getGridAttribute(view.node, ATTR_LAYOUT_ROW) == null) { |
| setGridAttribute(view.node, ATTR_LAYOUT_ROW, view.row); |
| } |
| } |
| } |
| } |
| |
| // Find the spacer which marks this column, and if found, mark it as a split |
| ViewData prevColumnSpacer = null; |
| for (ViewData view : mChildViews) { |
| if (view.column == newColumn - 1 && view.isColumnSpacer()) { |
| prevColumnSpacer = view; |
| break; |
| } |
| } |
| |
| // Process all existing grid elements: |
| // * Increase column numbers for all columns that have a hardcoded column number |
| // greater than the new column |
| // * Set an explicit column=0 where needed (TODO: Implement this) |
| // * Increase the columnSpan for all columns that overlap the newly inserted column edge |
| // * Split the spacer which defined the size of this column into two |
| // (and if not found, create a new spacer) |
| // |
| for (ViewData view : mChildViews) { |
| if (view == prevColumnSpacer) { |
| continue; |
| } |
| |
| INode node = view.node; |
| int column = view.column; |
| if (column > newColumn || (column == newColumn && view.node.getBounds().x2() > x)) { |
| // ALWAYS set the column, because |
| // (1) if it has been set, it needs to be corrected |
| // (2) if it has not been set, it needs to be set to cause this column |
| // to skip over the new column (there may be no views for the new |
| // column on this row). |
| // TODO: Enhance this such that we only set the column to a skip number |
| // where necessary, e.g. only on the FIRST view on this row following the |
| // skipped column! |
| |
| //if (getGridAttribute(node, ATTR_LAYOUT_COLUMN) != null) { |
| view.column += insertMarginColumn ? 2 : 1; |
| setGridAttribute(node, ATTR_LAYOUT_COLUMN, view.column); |
| //} |
| } else if (!view.isSpacer()) { |
| // Adjust the column span? We must increase it if |
| // (1) the new column is inside the range [column, column + columnSpan] |
| // (2) the new column is within the last cell in the column span, |
| // and the exact X location of the split is within the horizontal |
| // *bounds* of this node (provided it has gravity=left) |
| // (3) the new column is within the last cell and the cell has gravity |
| // right or gravity center |
| int endColumn = column + view.columnSpan; |
| if (endColumn > newColumn |
| || endColumn == newColumn && (view.node.getBounds().x2() > x |
| || GravityHelper.isConstrainedHorizontally(view.gravity) |
| && !GravityHelper.isLeftAligned(view.gravity))) { |
| // This cell spans the new insert position, so increment the column span |
| view.columnSpan += insertMarginColumn ? 2 : 1; |
| setColumnSpanAttribute(node, view.columnSpan); |
| } |
| } |
| } |
| |
| // Insert new spacer: |
| if (prevColumnSpacer != null) { |
| int px = getColumnWidth(newColumn - 1, 1); |
| if (insertMarginColumn || columnWidthDp == 0) { |
| px -= getColumnActualWidth(newColumn - 1); |
| } |
| int dp = mRulesEngine.pxToDp(px); |
| int remaining = dp - columnWidthDp; |
| if (remaining > 0) { |
| prevColumnSpacer.node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, |
| String.format(VALUE_N_DP, remaining)); |
| prevColumnSpacer.column = insertMarginColumn ? newColumn + 1 : newColumn; |
| setGridAttribute(prevColumnSpacer.node, ATTR_LAYOUT_COLUMN, |
| prevColumnSpacer.column); |
| } |
| } |
| |
| if (columnWidthDp > 0) { |
| int index = prevColumnSpacer != null ? prevColumnSpacer.index : -1; |
| |
| addSpacer(layout, index, 0, insertMarginColumn ? newColumn : newColumn - 1, |
| columnWidthDp, SPACER_SIZE_DP); |
| } |
| } |
| |
| /** |
| * Split a cell into two or three rows. |
| * |
| * @param newRow The row number to insert before |
| * @param insertMarginRow If false, then the cell at newRow -1 is split with the above |
| * part taking up exactly rowHeightDp dips. If true, then the row is split |
| * twice; the top part is the implicit height of the row, the new middle |
| * (margin) row is exactly the rowHeightDp size and the bottom column is |
| * the remaining space of the old cell. |
| * @param rowHeightDp The height of the row inserted before the new row (or if |
| * insertMarginRow is false, then the height of the margin row) |
| * @param y the y coordinate of the new row |
| */ |
| public void splitRow(int newRow, boolean insertMarginRow, int rowHeightDp, int y) { |
| actualRowCount++; |
| |
| // Insert a new row |
| if (declaredRowCount != UNDEFINED) { |
| declaredRowCount++; |
| if (insertMarginRow) { |
| declaredRowCount++; |
| } |
| setGridAttribute(layout, ATTR_ROW_COUNT, declaredRowCount); |
| } |
| |
| // Find the spacer which marks this row, and if found, mark it as a split |
| ViewData prevRowSpacer = null; |
| for (ViewData view : mChildViews) { |
| if (view.row == newRow - 1 && view.isRowSpacer()) { |
| prevRowSpacer = view; |
| break; |
| } |
| } |
| |
| // Se splitColumn() for details |
| for (ViewData view : mChildViews) { |
| if (view == prevRowSpacer) { |
| continue; |
| } |
| |
| INode node = view.node; |
| int row = view.row; |
| if (row > newRow || (row == newRow && view.node.getBounds().y2() > y)) { |
| //if (getGridAttribute(node, ATTR_LAYOUT_ROW) != null) { |
| view.row += insertMarginRow ? 2 : 1; |
| setGridAttribute(node, ATTR_LAYOUT_ROW, view.row); |
| //} |
| } else if (!view.isSpacer()) { |
| int endRow = row + view.rowSpan; |
| if (endRow > newRow |
| || endRow == newRow && (view.node.getBounds().y2() > y |
| || GravityHelper.isConstrainedVertically(view.gravity) |
| && !GravityHelper.isTopAligned(view.gravity))) { |
| // This cell spans the new insert position, so increment the row span |
| view.rowSpan += insertMarginRow ? 2 : 1; |
| setRowSpanAttribute(node, view.rowSpan); |
| } |
| } |
| } |
| |
| // Insert new spacer: |
| if (prevRowSpacer != null) { |
| int px = getRowHeight(newRow - 1, 1); |
| if (insertMarginRow || rowHeightDp == 0) { |
| px -= getRowActualHeight(newRow - 1); |
| } |
| int dp = mRulesEngine.pxToDp(px); |
| int remaining = dp - rowHeightDp; |
| if (remaining > 0) { |
| prevRowSpacer.node.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT, |
| String.format(VALUE_N_DP, remaining)); |
| prevRowSpacer.row = insertMarginRow ? newRow + 1 : newRow; |
| setGridAttribute(prevRowSpacer.node, ATTR_LAYOUT_ROW, prevRowSpacer.row); |
| } |
| } |
| |
| if (rowHeightDp > 0) { |
| int index = prevRowSpacer != null ? prevRowSpacer.index : -1; |
| addSpacer(layout, index, insertMarginRow ? newRow : newRow - 1, |
| 0, SPACER_SIZE_DP, rowHeightDp); |
| } |
| } |
| |
| /** |
| * Data about a view in a table; this is not the same as a cell because multiple views |
| * can share a single cell, and a view can span many cells. |
| */ |
| public class ViewData { |
| public final INode node; |
| public final int index; |
| public int row; |
| public int column; |
| public int rowSpan; |
| public int columnSpan; |
| public int gravity; |
| |
| ViewData(INode n, int index) { |
| node = n; |
| this.index = index; |
| |
| column = getGridAttribute(n, ATTR_LAYOUT_COLUMN, UNDEFINED); |
| columnSpan = getGridAttribute(n, ATTR_LAYOUT_COLUMN_SPAN, 1); |
| row = getGridAttribute(n, ATTR_LAYOUT_ROW, UNDEFINED); |
| rowSpan = getGridAttribute(n, ATTR_LAYOUT_ROW_SPAN, 1); |
| gravity = GravityHelper.getGravity(getGridAttribute(n, ATTR_LAYOUT_GRAVITY), 0); |
| } |
| |
| /** Applies the column and row fields into the XML model */ |
| void applyPositionAttributes() { |
| setGridAttribute(node, ATTR_LAYOUT_COLUMN, column); |
| setGridAttribute(node, ATTR_LAYOUT_ROW, row); |
| } |
| |
| /** Returns the id of this node, or makes one up for display purposes */ |
| String getId() { |
| String id = node.getStringAttr(ANDROID_URI, ATTR_ID); |
| if (id == null) { |
| id = "<unknownid>"; //$NON-NLS-1$ |
| String fqn = node.getFqcn(); |
| fqn = fqn.substring(fqn.lastIndexOf('.') + 1); |
| id = fqn + "-" |
| + Integer.toString(System.identityHashCode(node)).substring(0, 3); |
| } |
| |
| return id; |
| } |
| |
| /** Returns true if this {@link ViewData} represents a spacer */ |
| boolean isSpacer() { |
| return isSpace(node.getFqcn()); |
| } |
| |
| /** |
| * Returns true if this {@link ViewData} represents a column spacer |
| */ |
| boolean isColumnSpacer() { |
| return isSpacer() && |
| // Any spacer not found in column 0 is a column spacer since we |
| // place all horizontal spacers in column 0 |
| ((column > 0) |
| // TODO: Find a cleaner way. Maybe set ids on the elements in (0,0) and |
| // for column distinguish by id. Or at least only do this for column 0! |
| || !SPACER_SIZE.equals(node.getStringAttr(ANDROID_URI, ATTR_LAYOUT_WIDTH))); |
| } |
| |
| /** |
| * Returns true if this {@link ViewData} represents a row spacer |
| */ |
| boolean isRowSpacer() { |
| return isSpacer() && |
| // Any spacer not found in row 0 is a row spacer since we |
| // place all vertical spacers in row 0 |
| ((row > 0) |
| // TODO: Find a cleaner way. Maybe set ids on the elements in (0,0) and |
| // for column distinguish by id. Or at least only do this for column 0! |
| || !SPACER_SIZE.equals(node.getStringAttr(ANDROID_URI, ATTR_LAYOUT_HEIGHT))); |
| } |
| } |
| |
| /** |
| * Sets the column span of the given node to the given value (or if the value is 1, |
| * removes it) |
| * |
| * @param node the target node |
| * @param span the new column span |
| */ |
| public void setColumnSpanAttribute(INode node, int span) { |
| setGridAttribute(node, ATTR_LAYOUT_COLUMN_SPAN, span > 1 ? Integer.toString(span) : null); |
| } |
| |
| /** |
| * Sets the row span of the given node to the given value (or if the value is 1, |
| * removes it) |
| * |
| * @param node the target node |
| * @param span the new row span |
| */ |
| public void setRowSpanAttribute(INode node, int span) { |
| setGridAttribute(node, ATTR_LAYOUT_ROW_SPAN, span > 1 ? Integer.toString(span) : null); |
| } |
| |
| /** Returns the index of the given target node in the given child node array */ |
| static int getChildIndex(INode[] children, INode target) { |
| int index = 0; |
| for (INode child : children) { |
| if (child == target) { |
| return index; |
| } |
| index++; |
| } |
| |
| return -1; |
| } |
| |
| /** |
| * Update the model to account for the given nodes getting deleted. The nodes |
| * are not actually deleted by this method; that is assumed to be performed by the |
| * caller. Instead this method performs whatever model updates are necessary to |
| * preserve the grid structure. |
| * |
| * @param nodes the nodes to be deleted |
| */ |
| public void onDeleted(@NonNull List<INode> nodes) { |
| if (nodes.size() == 0) { |
| return; |
| } |
| |
| // Attempt to clean up spacer objects for any newly-empty rows or columns |
| // as the result of this deletion |
| |
| Set<INode> deleted = new HashSet<INode>(); |
| |
| for (INode child : nodes) { |
| // We don't care about deletion of spacers |
| String fqcn = child.getFqcn(); |
| if (fqcn.equals(FQCN_SPACE) || fqcn.equals(FQCN_SPACE_V7)) { |
| continue; |
| } |
| deleted.add(child); |
| } |
| |
| Set<Integer> usedColumns = new HashSet<Integer>(actualColumnCount); |
| Set<Integer> usedRows = new HashSet<Integer>(actualRowCount); |
| Multimap<Integer, ViewData> columnSpacers = ArrayListMultimap.create(actualColumnCount, 2); |
| Multimap<Integer, ViewData> rowSpacers = ArrayListMultimap.create(actualRowCount, 2); |
| Set<ViewData> removedViews = new HashSet<ViewData>(); |
| |
| for (ViewData view : mChildViews) { |
| if (deleted.contains(view.node)) { |
| removedViews.add(view); |
| } else if (view.isColumnSpacer()) { |
| columnSpacers.put(view.column, view); |
| } else if (view.isRowSpacer()) { |
| rowSpacers.put(view.row, view); |
| } else { |
| usedColumns.add(Integer.valueOf(view.column)); |
| usedRows.add(Integer.valueOf(view.row)); |
| } |
| } |
| |
| if (usedColumns.size() == 0 || usedRows.size() == 0) { |
| // No more views - just remove all the spacers |
| for (ViewData spacer : columnSpacers.values()) { |
| layout.removeChild(spacer.node); |
| } |
| for (ViewData spacer : rowSpacers.values()) { |
| layout.removeChild(spacer.node); |
| } |
| mChildViews.clear(); |
| actualColumnCount = 0; |
| declaredColumnCount = 2; |
| actualRowCount = 0; |
| declaredRowCount = UNDEFINED; |
| setGridAttribute(layout, ATTR_COLUMN_COUNT, 2); |
| |
| return; |
| } |
| |
| // Determine columns to introduce spacers into: |
| // This is tricky; I should NOT combine spacers if there are cells tied to |
| // individual ones |
| |
| // TODO: Invalidate column sizes too! Otherwise repeated updates might get confused! |
| // Similarly, inserts need to do the same! |
| |
| // Produce map of old column numbers to new column numbers |
| // Collapse regions of consecutive space and non-space ranges together |
| int[] columnMap = new int[actualColumnCount + 1]; // +1: Easily handle columnSpans as well |
| int newColumn = 0; |
| boolean prevUsed = usedColumns.contains(0); |
| for (int column = 1; column < actualColumnCount; column++) { |
| boolean used = usedColumns.contains(column); |
| if (used || prevUsed != used) { |
| newColumn++; |
| prevUsed = used; |
| } |
| columnMap[column] = newColumn; |
| } |
| newColumn++; |
| columnMap[actualColumnCount] = newColumn; |
| assert columnMap[0] == 0; |
| |
| int[] rowMap = new int[actualRowCount + 1]; // +1: Easily handle rowSpans as well |
| int newRow = 0; |
| prevUsed = usedRows.contains(0); |
| for (int row = 1; row < actualRowCount; row++) { |
| boolean used = usedRows.contains(row); |
| if (used || prevUsed != used) { |
| newRow++; |
| prevUsed = used; |
| } |
| rowMap[row] = newRow; |
| } |
| newRow++; |
| rowMap[actualRowCount] = newRow; |
| assert rowMap[0] == 0; |
| |
| |
| // Adjust column and row numbers to account for deletions: for a given cell, if it |
| // is to the right of a deleted column, reduce its column number, and if it only |
| // spans across the deleted column, reduce its column span. |
| for (ViewData view : mChildViews) { |
| if (removedViews.contains(view)) { |
| continue; |
| } |
| int newColumnStart = columnMap[Math.min(columnMap.length - 1, view.column)]; |
| // Gracefully handle rogue/invalid columnSpans in the XML |
| int newColumnEnd = columnMap[Math.min(columnMap.length - 1, |
| view.column + view.columnSpan)]; |
| if (newColumnStart != view.column) { |
| view.column = newColumnStart; |
| setGridAttribute(view.node, ATTR_LAYOUT_COLUMN, view.column); |
| } |
| |
| int columnSpan = newColumnEnd - newColumnStart; |
| if (columnSpan != view.columnSpan) { |
| if (columnSpan >= 1) { |
| view.columnSpan = columnSpan; |
| setColumnSpanAttribute(view.node, view.columnSpan); |
| } // else: merging spacing columns together |
| } |
| |
| |
| int newRowStart = rowMap[Math.min(rowMap.length - 1, view.row)]; |
| int newRowEnd = rowMap[Math.min(rowMap.length - 1, view.row + view.rowSpan)]; |
| if (newRowStart != view.row) { |
| view.row = newRowStart; |
| setGridAttribute(view.node, ATTR_LAYOUT_ROW, view.row); |
| } |
| |
| int rowSpan = newRowEnd - newRowStart; |
| if (rowSpan != view.rowSpan) { |
| if (rowSpan >= 1) { |
| view.rowSpan = rowSpan; |
| setRowSpanAttribute(view.node, view.rowSpan); |
| } // else: merging spacing rows together |
| } |
| } |
| |
| // Merge spacers (and add spacers for newly empty columns) |
| int start = 0; |
| while (start < actualColumnCount) { |
| // Find next unused span |
| while (start < actualColumnCount && usedColumns.contains(start)) { |
| start++; |
| } |
| if (start == actualColumnCount) { |
| break; |
| } |
| assert !usedColumns.contains(start); |
| // Find the next span of unused columns and produce a SINGLE |
| // spacer for that range (unless it's a zero-sized columns) |
| int end = start + 1; |
| for (; end < actualColumnCount; end++) { |
| if (usedColumns.contains(end)) { |
| break; |
| } |
| } |
| |
| // Add up column sizes |
| int width = getColumnWidth(start, end - start); |
| |
| // Find all spacers: the first one found should be moved to the start column |
| // and assigned to the full height of the columns, and |
| // the column count reduced by the corresponding amount |
| |
| // TODO: if width = 0, fully remove |
| |
| boolean isFirstSpacer = true; |
| for (int column = start; column < end; column++) { |
| Collection<ViewData> spacers = columnSpacers.get(column); |
| if (spacers != null && !spacers.isEmpty()) { |
| // Avoid ConcurrentModificationException since we're inserting into the |
| // map within this loop (always at a different index, but the map doesn't |
| // know that) |
| spacers = new ArrayList<ViewData>(spacers); |
| for (ViewData spacer : spacers) { |
| if (isFirstSpacer) { |
| isFirstSpacer = false; |
| spacer.column = columnMap[start]; |
| setGridAttribute(spacer.node, ATTR_LAYOUT_COLUMN, spacer.column); |
| if (end - start > 1) { |
| // Compute a merged width for all the spacers (not needed if |
| // there's just one spacer; it should already have the correct width) |
| int columnWidthDp = mRulesEngine.pxToDp(width); |
| spacer.node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, |
| String.format(VALUE_N_DP, columnWidthDp)); |
| } |
| columnSpacers.put(start, spacer); |
| } else { |
| removedViews.add(spacer); // Mark for model removal |
| layout.removeChild(spacer.node); |
| } |
| } |
| } |
| } |
| |
| if (isFirstSpacer) { |
| // No spacer: create one |
| int columnWidthDp = mRulesEngine.pxToDp(width); |
| addSpacer(layout, -1, UNDEFINED, columnMap[start], columnWidthDp, DEFAULT_CELL_HEIGHT); |
| } |
| |
| start = end; |
| } |
| actualColumnCount = newColumn; |
| //if (usedColumns.contains(newColumn)) { |
| // // TODO: This may be totally wrong for right aligned content! |
| // actualColumnCount++; |
| //} |
| |
| // Merge spacers for rows |
| start = 0; |
| while (start < actualRowCount) { |
| // Find next unused span |
| while (start < actualRowCount && usedRows.contains(start)) { |
| start++; |
| } |
| if (start == actualRowCount) { |
| break; |
| } |
| assert !usedRows.contains(start); |
| // Find the next span of unused rows and produce a SINGLE |
| // spacer for that range (unless it's a zero-sized rows) |
| int end = start + 1; |
| for (; end < actualRowCount; end++) { |
| if (usedRows.contains(end)) { |
| break; |
| } |
| } |
| |
| // Add up row sizes |
| int height = getRowHeight(start, end - start); |
| |
| // Find all spacers: the first one found should be moved to the start row |
| // and assigned to the full height of the rows, and |
| // the row count reduced by the corresponding amount |
| |
| // TODO: if width = 0, fully remove |
| |
| boolean isFirstSpacer = true; |
| for (int row = start; row < end; row++) { |
| Collection<ViewData> spacers = rowSpacers.get(row); |
| if (spacers != null && !spacers.isEmpty()) { |
| // Avoid ConcurrentModificationException since we're inserting into the |
| // map within this loop (always at a different index, but the map doesn't |
| // know that) |
| spacers = new ArrayList<ViewData>(spacers); |
| for (ViewData spacer : spacers) { |
| if (isFirstSpacer) { |
| isFirstSpacer = false; |
| spacer.row = rowMap[start]; |
| setGridAttribute(spacer.node, ATTR_LAYOUT_ROW, spacer.row); |
| if (end - start > 1) { |
| // Compute a merged width for all the spacers (not needed if |
| // there's just one spacer; it should already have the correct height) |
| int rowHeightDp = mRulesEngine.pxToDp(height); |
| spacer.node.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT, |
| String.format(VALUE_N_DP, rowHeightDp)); |
| } |
| rowSpacers.put(start, spacer); |
| } else { |
| removedViews.add(spacer); // Mark for model removal |
| layout.removeChild(spacer.node); |
| } |
| } |
| } |
| } |
| |
| if (isFirstSpacer) { |
| // No spacer: create one |
| int rowWidthDp = mRulesEngine.pxToDp(height); |
| addSpacer(layout, -1, rowMap[start], UNDEFINED, DEFAULT_CELL_WIDTH, rowWidthDp); |
| } |
| |
| start = end; |
| } |
| actualRowCount = newRow; |
| // if (usedRows.contains(newRow)) { |
| // actualRowCount++; |
| // } |
| |
| // Update the model: remove removed children from the view data list |
| if (removedViews.size() <= 2) { |
| mChildViews.removeAll(removedViews); |
| } else { |
| List<ViewData> remaining = |
| new ArrayList<ViewData>(mChildViews.size() - removedViews.size()); |
| for (ViewData view : mChildViews) { |
| if (!removedViews.contains(view)) { |
| remaining.add(view); |
| } |
| } |
| mChildViews = remaining; |
| } |
| |
| // Update the final column and row declared attributes |
| if (declaredColumnCount != UNDEFINED) { |
| declaredColumnCount = actualColumnCount; |
| setGridAttribute(layout, ATTR_COLUMN_COUNT, actualColumnCount); |
| } |
| if (declaredRowCount != UNDEFINED) { |
| declaredRowCount = actualRowCount; |
| setGridAttribute(layout, ATTR_ROW_COUNT, actualRowCount); |
| } |
| } |
| |
| /** |
| * Adds a spacer to the given parent, at the given index. |
| * |
| * @param parent the GridLayout |
| * @param index the index to insert the spacer at, or -1 to append |
| * @param row the row to add the spacer to (or {@link #UNDEFINED} to not set a row yet |
| * @param column the column to add the spacer to (or {@link #UNDEFINED} to not set a |
| * column yet |
| * @param widthDp the width in device independent pixels to assign to the spacer |
| * @param heightDp the height in device independent pixels to assign to the spacer |
| * @return the newly added spacer |
| */ |
| ViewData addSpacer(INode parent, int index, int row, int column, |
| int widthDp, int heightDp) { |
| INode spacer; |
| |
| String tag = FQCN_SPACE; |
| String gridLayout = parent.getFqcn(); |
| if (!gridLayout.equals(GRID_LAYOUT) && gridLayout.length() > GRID_LAYOUT.length()) { |
| String pkg = gridLayout.substring(0, gridLayout.length() - GRID_LAYOUT.length()); |
| tag = pkg + SPACE; |
| } |
| if (index != -1) { |
| spacer = parent.insertChildAt(tag, index); |
| } else { |
| spacer = parent.appendChild(tag); |
| } |
| |
| ViewData view = new ViewData(spacer, index != -1 ? index : mChildViews.size()); |
| mChildViews.add(view); |
| |
| if (row != UNDEFINED) { |
| view.row = row; |
| setGridAttribute(spacer, ATTR_LAYOUT_ROW, row); |
| } |
| if (column != UNDEFINED) { |
| view.column = column; |
| setGridAttribute(spacer, ATTR_LAYOUT_COLUMN, column); |
| } |
| if (widthDp > 0) { |
| spacer.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, |
| String.format(VALUE_N_DP, widthDp)); |
| } |
| if (heightDp > 0) { |
| spacer.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT, |
| String.format(VALUE_N_DP, heightDp)); |
| } |
| |
| // Temporary hack |
| if (GridLayoutRule.sDebugGridLayout) { |
| //String id = NEW_ID_PREFIX + "s"; |
| //if (row == 0) { |
| // id += "c"; |
| //} |
| //if (column == 0) { |
| // id += "r"; |
| //} |
| //if (row > 0) { |
| // id += Integer.toString(row); |
| //} |
| //if (column > 0) { |
| // id += Integer.toString(column); |
| //} |
| String id = NEW_ID_PREFIX + "spacer_" //$NON-NLS-1$ |
| + Integer.toString(System.identityHashCode(spacer)).substring(0, 3); |
| spacer.setAttribute(ANDROID_URI, ATTR_ID, id); |
| } |
| |
| |
| return view; |
| } |
| |
| /** |
| * Returns the string value of the given attribute, or null if it does not |
| * exist. This only works for attributes that are GridLayout specific, such |
| * as columnCount, layout_column, layout_row_span, etc. |
| * |
| * @param node the target node |
| * @param name the attribute name (which must be in the android: namespace) |
| * @return the attribute value or null |
| */ |
| |
| public String getGridAttribute(INode node, String name) { |
| return node.getStringAttr(getNamespace(), name); |
| } |
| |
| /** |
| * Returns the integer value of the given attribute, or the given defaultValue if the |
| * attribute was not set. This only works for attributes that are GridLayout specific, |
| * such as columnCount, layout_column, layout_row_span, etc. |
| * |
| * @param node the target node |
| * @param attribute the attribute name (which must be in the android: namespace) |
| * @param defaultValue the default value to use if the value is not set |
| * @return the attribute integer value |
| */ |
| private int getGridAttribute(INode node, String attribute, int defaultValue) { |
| String valueString = node.getStringAttr(getNamespace(), attribute); |
| if (valueString != null) { |
| try { |
| return Integer.decode(valueString); |
| } catch (NumberFormatException nufe) { |
| // Ignore - error in user's XML |
| } |
| } |
| |
| return defaultValue; |
| } |
| |
| /** |
| * Returns the number of children views in the GridLayout |
| * |
| * @return the number of children views in the GridLayout |
| */ |
| public int getViewCount() { |
| return mChildViews.size(); |
| } |
| |
| /** |
| * Returns true if the given class name represents a spacer |
| * |
| * @param fqcn the fully qualified class name |
| * @return true if this is a spacer |
| */ |
| public static boolean isSpace(String fqcn) { |
| return FQCN_SPACE.equals(fqcn) || FQCN_SPACE_V7.equals(fqcn); |
| } |
| } |