/* * Copyright (C) 2010 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; import static com.android.SdkConstants.ANDROID_URI; import static com.android.SdkConstants.ATTR_BASELINE_ALIGNED; import static com.android.SdkConstants.ATTR_LAYOUT_GRAVITY; import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT; import static com.android.SdkConstants.ATTR_LAYOUT_WEIGHT; import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH; import static com.android.SdkConstants.ATTR_ORIENTATION; import static com.android.SdkConstants.ATTR_WEIGHT_SUM; import static com.android.SdkConstants.VALUE_1; import static com.android.SdkConstants.VALUE_HORIZONTAL; import static com.android.SdkConstants.VALUE_VERTICAL; import static com.android.SdkConstants.VALUE_WRAP_CONTENT; import static com.android.SdkConstants.VALUE_ZERO_DP; import static com.android.ide.eclipse.adt.AdtUtils.formatFloatAttribute; import com.android.SdkConstants; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.ide.common.api.DrawingStyle; import com.android.ide.common.api.DropFeedback; import com.android.ide.common.api.IClientRulesEngine; import com.android.ide.common.api.IDragElement; import com.android.ide.common.api.IFeedbackPainter; import com.android.ide.common.api.IGraphics; import com.android.ide.common.api.IMenuCallback; import com.android.ide.common.api.INode; import com.android.ide.common.api.INodeHandler; import com.android.ide.common.api.IViewMetadata; import com.android.ide.common.api.IViewMetadata.FillPreference; import com.android.ide.common.api.IViewRule; import com.android.ide.common.api.InsertType; import com.android.ide.common.api.Point; import com.android.ide.common.api.Rect; import com.android.ide.common.api.RuleAction; import com.android.ide.common.api.RuleAction.Choices; import com.android.ide.common.api.SegmentType; import com.android.ide.eclipse.adt.AdtPlugin; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; /** * An {@link IViewRule} for android.widget.LinearLayout and all its derived * classes. */ public class LinearLayoutRule extends BaseLayoutRule { private static final String ACTION_ORIENTATION = "_orientation"; //$NON-NLS-1$ private static final String ACTION_WEIGHT = "_weight"; //$NON-NLS-1$ private static final String ACTION_DISTRIBUTE = "_distribute"; //$NON-NLS-1$ private static final String ACTION_BASELINE = "_baseline"; //$NON-NLS-1$ private static final String ACTION_CLEAR = "_clear"; //$NON-NLS-1$ private static final String ACTION_DOMINATE = "_dominate"; //$NON-NLS-1$ private static final URL ICON_HORIZONTAL = LinearLayoutRule.class.getResource("hlinear.png"); //$NON-NLS-1$ private static final URL ICON_VERTICAL = LinearLayoutRule.class.getResource("vlinear.png"); //$NON-NLS-1$ private static final URL ICON_WEIGHTS = LinearLayoutRule.class.getResource("weights.png"); //$NON-NLS-1$ private static final URL ICON_DISTRIBUTE = LinearLayoutRule.class.getResource("distribute.png"); //$NON-NLS-1$ private static final URL ICON_BASELINE = LinearLayoutRule.class.getResource("baseline.png"); //$NON-NLS-1$ private static final URL ICON_CLEAR_WEIGHTS = LinearLayoutRule.class.getResource("clearweights.png"); //$NON-NLS-1$ private static final URL ICON_DOMINATE = LinearLayoutRule.class.getResource("allweight.png"); //$NON-NLS-1$ /** * Returns the current orientation, regardless of whether it has been defined in XML * * @param node The LinearLayout to look up the orientation for * @return "horizontal" or "vertical" depending on the current orientation of the * linear layout */ private String getCurrentOrientation(final INode node) { String orientation = node.getStringAttr(ANDROID_URI, ATTR_ORIENTATION); if (orientation == null || orientation.length() == 0) { orientation = VALUE_HORIZONTAL; } return orientation; } /** * Returns true if the given node represents a vertical linear layout. * @param node the node to check layout orientation for * @return true if the layout is in vertical mode, otherwise false */ protected boolean isVertical(INode node) { // Horizontal is the default, so if no value is specified it is horizontal. return VALUE_VERTICAL.equals(node.getStringAttr(ANDROID_URI, ATTR_ORIENTATION)); } /** * Returns true if this LinearLayout supports switching orientation. * * @return true if this layout supports orientations */ protected boolean supportsOrientation() { return true; } @Override public void addLayoutActions( @NonNull List<RuleAction> actions, final @NonNull INode parentNode, final @NonNull List<? extends INode> children) { super.addLayoutActions(actions, parentNode, children); if (supportsOrientation()) { Choices action = RuleAction.createChoices( ACTION_ORIENTATION, "Orientation", //$NON-NLS-1$ new PropertyCallback(Collections.singletonList(parentNode), "Change LinearLayout Orientation", ANDROID_URI, ATTR_ORIENTATION), Arrays.<String>asList("Set Horizontal Orientation","Set Vertical Orientation"), Arrays.<URL>asList(ICON_HORIZONTAL, ICON_VERTICAL), Arrays.<String>asList("horizontal", "vertical"), getCurrentOrientation(parentNode), null /* icon */, -10, false /* supportsMultipleNodes */ ); action.setRadio(true); actions.add(action); } if (!isVertical(parentNode)) { String current = parentNode.getStringAttr(ANDROID_URI, ATTR_BASELINE_ALIGNED); boolean isAligned = current == null || Boolean.valueOf(current); actions.add(RuleAction.createToggle(ACTION_BASELINE, "Toggle Baseline Alignment", isAligned, new PropertyCallback(Collections.singletonList(parentNode), "Change Baseline Alignment", ANDROID_URI, ATTR_BASELINE_ALIGNED), // TODO: Also set index? ICON_BASELINE, 38, false)); } // Gravity if (children != null && children.size() > 0) { actions.add(RuleAction.createSeparator(35)); // Margins actions.add(createMarginAction(parentNode, children)); // Gravity actions.add(createGravityAction(children, ATTR_LAYOUT_GRAVITY)); // Weights IMenuCallback actionCallback = new IMenuCallback() { @Override public void action( final @NonNull RuleAction action, @NonNull List<? extends INode> selectedNodes, final @Nullable String valueId, final @Nullable Boolean newValue) { parentNode.editXml("Change Weight", new INodeHandler() { @Override public void handle(@NonNull INode n) { String id = action.getId(); if (id.equals(ACTION_WEIGHT)) { String weight = children.get(0).getStringAttr(ANDROID_URI, ATTR_LAYOUT_WEIGHT); if (weight == null || weight.length() == 0) { weight = "0.0"; //$NON-NLS-1$ } weight = mRulesEngine.displayInput("Enter Weight Value:", weight, null); if (weight != null) { if (weight.isEmpty()) { weight = null; // remove attribute } for (INode child : children) { child.setAttribute(ANDROID_URI, ATTR_LAYOUT_WEIGHT, weight); } } } else if (id.equals(ACTION_DISTRIBUTE)) { distributeWeights(parentNode, parentNode.getChildren()); } else if (id.equals(ACTION_CLEAR)) { clearWeights(parentNode); } else if (id.equals(ACTION_CLEAR) || id.equals(ACTION_DOMINATE)) { clearWeights(parentNode); distributeWeights(parentNode, children.toArray(new INode[children.size()])); } else { assert id.equals(ACTION_BASELINE); } } }); } }; actions.add(RuleAction.createSeparator(50)); actions.add(RuleAction.createAction(ACTION_DISTRIBUTE, "Distribute Weights Evenly", actionCallback, ICON_DISTRIBUTE, 60, false /*supportsMultipleNodes*/)); actions.add(RuleAction.createAction(ACTION_DOMINATE, "Assign All Weight", actionCallback, ICON_DOMINATE, 70, false)); actions.add(RuleAction.createAction(ACTION_WEIGHT, "Change Layout Weight", actionCallback, ICON_WEIGHTS, 80, false)); actions.add(RuleAction.createAction(ACTION_CLEAR, "Clear All Weights", actionCallback, ICON_CLEAR_WEIGHTS, 90, false)); } } private void distributeWeights(INode parentNode, INode[] targets) { // Any XML to get weight sum? String weightSum = parentNode.getStringAttr(ANDROID_URI, ATTR_WEIGHT_SUM); double sum = -1.0; if (weightSum != null) { // Distribute try { sum = Double.parseDouble(weightSum); } catch (NumberFormatException nfe) { // Just keep using the default } } int numTargets = targets.length; double share; if (sum <= 0.0) { // The sum will be computed from the children, so just // use arbitrary amount share = 1.0; } else { share = sum / numTargets; } String value = formatFloatAttribute((float) share); String sizeAttribute = isVertical(parentNode) ? ATTR_LAYOUT_HEIGHT : ATTR_LAYOUT_WIDTH; for (INode target : targets) { target.setAttribute(ANDROID_URI, ATTR_LAYOUT_WEIGHT, value); // Also set the width/height to 0dp to ensure actual equal // size (without this, only the remaining space is // distributed) if (VALUE_WRAP_CONTENT.equals(target.getStringAttr(ANDROID_URI, sizeAttribute))) { target.setAttribute(ANDROID_URI, sizeAttribute, VALUE_ZERO_DP); } } } private void clearWeights(INode parentNode) { // Clear attributes String sizeAttribute = isVertical(parentNode) ? ATTR_LAYOUT_HEIGHT : ATTR_LAYOUT_WIDTH; for (INode target : parentNode.getChildren()) { target.setAttribute(ANDROID_URI, ATTR_LAYOUT_WEIGHT, null); String size = target.getStringAttr(ANDROID_URI, sizeAttribute); if (size != null && size.startsWith("0")) { //$NON-NLS-1$ target.setAttribute(ANDROID_URI, sizeAttribute, VALUE_WRAP_CONTENT); } } } // ==== Drag'n'drop support ==== @Override public DropFeedback onDropEnter(final @NonNull INode targetNode, @Nullable Object targetView, final @Nullable IDragElement[] elements) { if (elements.length == 0) { return null; } Rect bn = targetNode.getBounds(); if (!bn.isValid()) { return null; } boolean isVertical = isVertical(targetNode); // Prepare a list of insertion points: X coords for horizontal, Y for // vertical. List<MatchPos> indexes = new ArrayList<MatchPos>(); int last = isVertical ? bn.y : bn.x; int pos = 0; boolean lastDragged = false; int selfPos = -1; for (INode it : targetNode.getChildren()) { Rect bc = it.getBounds(); if (bc.isValid()) { // First see if this node looks like it's the same as one of the // *dragged* bounds boolean isDragged = false; for (IDragElement element : elements) { // This tries to determine if an INode corresponds to an // IDragElement, by comparing their bounds. if (element.isSame(it)) { isDragged = true; break; } } // We don't want to insert drag positions before or after the // element that is itself being dragged. However, we -do- want // to insert a match position here, at the center, such that // when you drag near its current position we show a match right // where it's already positioned. if (isDragged) { int v = isVertical ? bc.y + (bc.h / 2) : bc.x + (bc.w / 2); selfPos = pos; indexes.add(new MatchPos(v, pos++)); } else if (lastDragged) { // Even though we don't want to insert a match below, we // need to increment the index counter such that subsequent // lines know their correct index in the child list. pos++; } else { // Add an insertion point between the last point and the // start of this child int v = isVertical ? bc.y : bc.x; v = (last + v) / 2; indexes.add(new MatchPos(v, pos++)); } last = isVertical ? (bc.y + bc.h) : (bc.x + bc.w); lastDragged = isDragged; } else { // We still have to count this position even if it has no bounds, or // subsequent children will be inserted at the wrong place pos++; } } // Finally add an insert position after all the children - unless of // course we happened to be dragging the last element if (!lastDragged) { int v = last + 1; indexes.add(new MatchPos(v, pos)); } int posCount = targetNode.getChildren().length + 1; return new DropFeedback(new LinearDropData(indexes, posCount, isVertical, selfPos), new IFeedbackPainter() { @Override public void paint(@NonNull IGraphics gc, @NonNull INode node, @NonNull DropFeedback feedback) { // Paint callback for the LinearLayout. This is called // by the canvas when a draw is needed. drawFeedback(gc, node, elements, feedback); } }); } void drawFeedback(IGraphics gc, INode node, IDragElement[] elements, DropFeedback feedback) { Rect b = node.getBounds(); if (!b.isValid()) { return; } // Highlight the receiver gc.useStyle(DrawingStyle.DROP_RECIPIENT); gc.drawRect(b); gc.useStyle(DrawingStyle.DROP_ZONE); LinearDropData data = (LinearDropData) feedback.userData; boolean isVertical = data.isVertical(); int selfPos = data.getSelfPos(); for (MatchPos it : data.getIndexes()) { int i = it.getDistance(); int pos = it.getPosition(); // Don't show insert drop zones for "self"-index since that one goes // right through the center of the widget rather than in a sibling // position if (pos != selfPos) { if (isVertical) { // draw horizontal lines gc.drawLine(b.x, i, b.x + b.w, i); } else { // draw vertical lines gc.drawLine(i, b.y, i, b.y + b.h); } } } Integer currX = data.getCurrX(); Integer currY = data.getCurrY(); if (currX != null && currY != null) { gc.useStyle(DrawingStyle.DROP_ZONE_ACTIVE); int x = currX; int y = currY; Rect be = elements[0].getBounds(); // Draw a clear line at the closest drop zone (unless we're over the // dragged element itself) if (data.getInsertPos() != selfPos || selfPos == -1) { gc.useStyle(DrawingStyle.DROP_PREVIEW); if (data.getWidth() != null) { int width = data.getWidth(); int fromX = x - width / 2; int toX = x + width / 2; gc.drawLine(fromX, y, toX, y); } else if (data.getHeight() != null) { int height = data.getHeight(); int fromY = y - height / 2; int toY = y + height / 2; gc.drawLine(x, fromY, x, toY); } } if (be.isValid()) { boolean isLast = data.isLastPosition(); // At least the first element has a bound. Draw rectangles for // all dropped elements with valid bounds, offset at the drop // point. int offsetX; int offsetY; if (isVertical) { offsetX = b.x - be.x; offsetY = currY - be.y - (isLast ? 0 : (be.h / 2)); } else { offsetX = currX - be.x - (isLast ? 0 : (be.w / 2)); offsetY = b.y - be.y; } gc.useStyle(DrawingStyle.DROP_PREVIEW); for (IDragElement element : elements) { Rect bounds = element.getBounds(); if (bounds.isValid() && (bounds.w > b.w || bounds.h > b.h) && node.getChildren().length == 0) { // The bounds of the child does not fully fit inside the target. // Limit the bounds to the layout bounds (but only when there // are no children, since otherwise positioning around the existing // children gets difficult) final int px, py, pw, ph; if (bounds.w > b.w) { px = b.x; pw = b.w; } else { px = bounds.x + offsetX; pw = bounds.w; } if (bounds.h > b.h) { py = b.y; ph = b.h; } else { py = bounds.y + offsetY; ph = bounds.h; } Rect within = new Rect(px, py, pw, ph); gc.drawRect(within); } else { drawElement(gc, element, offsetX, offsetY); } } } } } @Override public DropFeedback onDropMove(@NonNull INode targetNode, @NonNull IDragElement[] elements, @Nullable DropFeedback feedback, @NonNull Point p) { Rect b = targetNode.getBounds(); if (!b.isValid()) { return feedback; } LinearDropData data = (LinearDropData) feedback.userData; boolean isVertical = data.isVertical(); int bestDist = Integer.MAX_VALUE; int bestIndex = Integer.MIN_VALUE; Integer bestPos = null; for (MatchPos index : data.getIndexes()) { int i = index.getDistance(); int pos = index.getPosition(); int dist = (isVertical ? p.y : p.x) - i; if (dist < 0) dist = -dist; if (dist < bestDist) { bestDist = dist; bestIndex = i; bestPos = pos; if (bestDist <= 0) break; } } if (bestIndex != Integer.MIN_VALUE) { Integer oldX = data.getCurrX(); Integer oldY = data.getCurrY(); if (isVertical) { data.setCurrX(b.x + b.w / 2); data.setCurrY(bestIndex); data.setWidth(b.w); data.setHeight(null); } else { data.setCurrX(bestIndex); data.setCurrY(b.y + b.h / 2); data.setWidth(null); data.setHeight(b.h); } data.setInsertPos(bestPos); feedback.requestPaint = !equals(oldX, data.getCurrX()) || !equals(oldY, data.getCurrY()); } return feedback; } private static boolean equals(Integer i1, Integer i2) { if (i1 == i2) { return true; } else if (i1 != null) { return i1.equals(i2); } else { // We know i2 != null return i2.equals(i1); } } @Override public void onDropLeave(@NonNull INode targetNode, @NonNull IDragElement[] elements, @Nullable DropFeedback feedback) { // ignore } @Override public void onDropped(final @NonNull INode targetNode, final @NonNull IDragElement[] elements, final @Nullable DropFeedback feedback, final @NonNull Point p) { LinearDropData data = (LinearDropData) feedback.userData; final int initialInsertPos = data.getInsertPos(); insertAt(targetNode, elements, feedback.isCopy || !feedback.sameCanvas, initialInsertPos); } @Override public void onChildInserted(@NonNull INode node, @NonNull INode parent, @NonNull InsertType insertType) { if (insertType == InsertType.MOVE_WITHIN) { // Don't adjust widths/heights/weights when just moving within a single // LinearLayout return; } // Attempt to set fill-properties on newly added views such that for example, // in a vertical layout, a text field defaults to filling horizontally, but not // vertically. String fqcn = node.getFqcn(); IViewMetadata metadata = mRulesEngine.getMetadata(fqcn); if (metadata != null) { boolean vertical = isVertical(parent); FillPreference fill = metadata.getFillPreference(); String fillParent = getFillParentValueName(); if (fill.fillHorizontally(vertical)) { node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, fillParent); } else if (!vertical && fill == FillPreference.WIDTH_IN_VERTICAL) { // In a horizontal layout, make views that would fill horizontally in a // vertical layout have a non-zero weight instead. This will make the item // fill but only enough to allow other views to be shown as well. // (However, for drags within the same layout we do not touch // the weight, since it might already have been tweaked to a particular // value) node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WEIGHT, VALUE_1); } if (fill.fillVertically(vertical)) { node.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT, fillParent); } } // If you insert into a layout that already is using layout weights, // and all the layout weights are the same (nonzero) value, then use // the same weight for this new layout as well. Also duplicate the 0dip/0px/0dp // sizes, if used. boolean duplicateWeight = true; boolean duplicate0dip = true; String sameWeight = null; String sizeAttribute = isVertical(parent) ? ATTR_LAYOUT_HEIGHT : ATTR_LAYOUT_WIDTH; for (INode target : parent.getChildren()) { if (target == node) { continue; } String weight = target.getStringAttr(ANDROID_URI, ATTR_LAYOUT_WEIGHT); if (weight == null || weight.length() == 0) { duplicateWeight = false; break; } else if (sameWeight != null && !sameWeight.equals(weight)) { duplicateWeight = false; } else { sameWeight = weight; } String size = target.getStringAttr(ANDROID_URI, sizeAttribute); if (size != null && !size.startsWith("0")) { //$NON-NLS-1$ duplicate0dip = false; break; } } if (duplicateWeight && sameWeight != null) { node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WEIGHT, sameWeight); if (duplicate0dip) { node.setAttribute(ANDROID_URI, sizeAttribute, VALUE_ZERO_DP); } } } /** A possible match position */ private static class MatchPos { /** The pixel distance */ private int mDistance; /** The position among siblings */ private int mPosition; public MatchPos(int distance, int position) { mDistance = distance; mPosition = position; } @Override public String toString() { return "MatchPos [distance=" + mDistance //$NON-NLS-1$ + ", position=" + mPosition //$NON-NLS-1$ + "]"; //$NON-NLS-1$ } private int getDistance() { return mDistance; } private int getPosition() { return mPosition; } } private static class LinearDropData { /** Vertical layout? */ private final boolean mVertical; /** Insert points (pixels + index) */ private final List<MatchPos> mIndexes; /** Number of insert positions in the target node */ private final int mNumPositions; /** Current marker X position */ private Integer mCurrX; /** Current marker Y position */ private Integer mCurrY; /** Position of the dragged element in this layout (or -1 if the dragged element is from elsewhere) */ private final int mSelfPos; /** Current drop insert index (-1 for "at the end") */ private int mInsertPos = -1; /** width of match line if it's a horizontal one */ private Integer mWidth; /** height of match line if it's a vertical one */ private Integer mHeight; public LinearDropData(List<MatchPos> indexes, int numPositions, boolean isVertical, int selfPos) { mIndexes = indexes; mNumPositions = numPositions; mVertical = isVertical; mSelfPos = selfPos; } @Override public String toString() { return "LinearDropData [currX=" + mCurrX //$NON-NLS-1$ + ", currY=" + mCurrY //$NON-NLS-1$ + ", height=" + mHeight //$NON-NLS-1$ + ", indexes=" + mIndexes //$NON-NLS-1$ + ", insertPos=" + mInsertPos //$NON-NLS-1$ + ", isVertical=" + mVertical //$NON-NLS-1$ + ", selfPos=" + mSelfPos //$NON-NLS-1$ + ", width=" + mWidth //$NON-NLS-1$ + "]"; //$NON-NLS-1$ } private boolean isVertical() { return mVertical; } private void setCurrX(Integer currX) { mCurrX = currX; } private Integer getCurrX() { return mCurrX; } private void setCurrY(Integer currY) { mCurrY = currY; } private Integer getCurrY() { return mCurrY; } private int getSelfPos() { return mSelfPos; } private void setInsertPos(int insertPos) { mInsertPos = insertPos; } private int getInsertPos() { return mInsertPos; } private List<MatchPos> getIndexes() { return mIndexes; } private void setWidth(Integer width) { mWidth = width; } private Integer getWidth() { return mWidth; } private void setHeight(Integer height) { mHeight = height; } private Integer getHeight() { return mHeight; } /** * Returns true if we are inserting into the last position * * @return true if we are inserting into the last position */ public boolean isLastPosition() { return mInsertPos == mNumPositions - 1; } } /** Custom resize state used during linear layout resizing */ private class LinearResizeState extends ResizeState { /** Whether the node should be assigned a new weight */ public boolean useWeight; /** Weight sum to be applied to the parent */ private float mNewWeightSum; /** The weight to be set on the node (provided {@link #useWeight} is true) */ private float mWeight; /** Map from nodes to preferred bounds of nodes where the weights have been cleared */ public final Map<INode, Rect> unweightedSizes; /** Total required size required by the siblings <b>without</b> weights */ public int totalLength; /** List of nodes which should have their weights cleared */ public List<INode> mClearWeights; private LinearResizeState(BaseLayoutRule rule, INode layout, Object layoutView, INode node) { super(rule, layout, layoutView, node); unweightedSizes = mRulesEngine.measureChildren(layout, new IClientRulesEngine.AttributeFilter() { @Override public String getAttribute(@NonNull INode n, @Nullable String namespace, @NonNull String localName) { // Clear out layout weights; we need to measure the unweighted sizes // of the children if (ATTR_LAYOUT_WEIGHT.equals(localName) && SdkConstants.NS_RESOURCES.equals(namespace)) { return ""; //$NON-NLS-1$ } return null; } }); // Compute total required size required by the siblings *without* weights totalLength = 0; final boolean isVertical = isVertical(layout); for (Map.Entry<INode, Rect> entry : unweightedSizes.entrySet()) { Rect preferredSize = entry.getValue(); if (isVertical) { totalLength += preferredSize.h; } else { totalLength += preferredSize.w; } } } /** Resets the computed state */ void reset() { mNewWeightSum = -1; useWeight = false; mClearWeights = null; } /** Sets a weight to be applied to the node */ void setWeight(float weight) { useWeight = true; mWeight = weight; } /** Sets a weight sum to be applied to the parent layout */ void setWeightSum(float weightSum) { mNewWeightSum = weightSum; } /** Marks that the given node should be cleared when applying the new size */ void clearWeight(INode n) { if (mClearWeights == null) { mClearWeights = new ArrayList<INode>(); } mClearWeights.add(n); } /** Applies the state to the nodes */ public void apply() { assert useWeight; String value = mWeight > 0 ? formatFloatAttribute(mWeight) : null; node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WEIGHT, value); if (mClearWeights != null) { for (INode n : mClearWeights) { if (getWeight(n) > 0.0f) { n.setAttribute(ANDROID_URI, ATTR_LAYOUT_WEIGHT, null); } } } if (mNewWeightSum > 0.0) { layout.setAttribute(ANDROID_URI, ATTR_WEIGHT_SUM, formatFloatAttribute(mNewWeightSum)); } } } @Override protected ResizeState createResizeState(INode layout, Object layoutView, INode node) { return new LinearResizeState(this, layout, layoutView, node); } protected void updateResizeState(LinearResizeState resizeState, final INode node, INode layout, Rect oldBounds, Rect newBounds, SegmentType horizontalEdge, SegmentType verticalEdge) { // Update the resize state. // This method attempts to compute a new layout weight to be used in the direction // of the linear layout. If the superclass has already determined that we can snap to // a wrap_content or match_parent boundary, we prefer that. Otherwise, we attempt to // compute a layout weight - which can fail if the size is too big (not enough room), // or if the size is too small (smaller than the natural width of the node), and so on. // In that case this method just aborts, which will leave the resize state object // in such a state that it will call the superclass to resize instead, which will fall // back to device independent pixel sizing. resizeState.reset(); if (oldBounds.equals(newBounds)) { return; } // If we're setting the width/height to wrap_content/match_parent in the dimension of the // linear layout, then just apply wrap_content and clear weights. boolean isVertical = isVertical(layout); if (!isVertical && verticalEdge != null) { if (resizeState.wrapWidth || resizeState.fillWidth) { resizeState.clearWeight(node); return; } if (newBounds.w == oldBounds.w) { return; } } if (isVertical && horizontalEdge != null) { if (resizeState.wrapHeight || resizeState.fillHeight) { resizeState.clearWeight(node); return; } if (newBounds.h == oldBounds.h) { return; } } // Compute weight sum float sum = getWeightSum(layout); if (sum <= 0.0f) { sum = 1.0f; resizeState.setWeightSum(sum); } // If the new size of the node is smaller than its preferred/wrap_content size, // then we cannot use weights to size it; switch to pixel-based sizing instead Map<INode, Rect> sizes = resizeState.unweightedSizes; Rect nodePreferredSize = sizes.get(node); if (nodePreferredSize != null) { if (horizontalEdge != null && newBounds.h < nodePreferredSize.h || verticalEdge != null && newBounds.w < nodePreferredSize.w) { return; } } Rect layoutBounds = layout.getBounds(); int remaining = (isVertical ? layoutBounds.h : layoutBounds.w) - resizeState.totalLength; Rect nodeBounds = sizes.get(node); if (nodeBounds == null) { return; } if (remaining > 0) { int missing = 0; if (isVertical) { if (newBounds.h > nodeBounds.h) { missing = newBounds.h - nodeBounds.h; } else if (newBounds.h > resizeState.wrapBounds.h) { // The weights concern how much space to ADD to the view. // What if we have resized it to a size *smaller* than its current // size without the weight delta? This can happen if you for example // have set a hardcoded size, such as 500dp, and then size it to some // smaller size. missing = newBounds.h - resizeState.wrapBounds.h; remaining += nodeBounds.h - resizeState.wrapBounds.h; resizeState.wrapHeight = true; } } else { if (newBounds.w > nodeBounds.w) { missing = newBounds.w - nodeBounds.w; } else if (newBounds.w > resizeState.wrapBounds.w) { missing = newBounds.w - resizeState.wrapBounds.w; remaining += nodeBounds.w - resizeState.wrapBounds.w; resizeState.wrapWidth = true; } } if (missing > 0) { // (weight / weightSum) * remaining = missing, so // weight = missing * weightSum / remaining float weight = missing * sum / remaining; resizeState.setWeight(weight); } } } /** * {@inheritDoc} * <p> * Overridden in this layout in order to make resizing affect the layout_weight * attribute instead of the layout_width (for horizontal LinearLayouts) or * layout_height (for vertical LinearLayouts). */ @Override protected void setNewSizeBounds(ResizeState state, final INode node, INode layout, Rect oldBounds, Rect newBounds, SegmentType horizontalEdge, SegmentType verticalEdge) { LinearResizeState resizeState = (LinearResizeState) state; updateResizeState(resizeState, node, layout, oldBounds, newBounds, horizontalEdge, verticalEdge); if (resizeState.useWeight) { resizeState.apply(); // Handle resizing in the opposite dimension of the layout final boolean isVertical = isVertical(layout); if (!isVertical && horizontalEdge != null) { if (newBounds.h != oldBounds.h || resizeState.wrapHeight || resizeState.fillHeight) { node.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT, resizeState.getHeightAttribute()); } } if (isVertical && verticalEdge != null) { if (newBounds.w != oldBounds.w || resizeState.wrapWidth || resizeState.fillWidth) { node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, resizeState.getWidthAttribute()); } } } else { node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WEIGHT, null); super.setNewSizeBounds(resizeState, node, layout, oldBounds, newBounds, horizontalEdge, verticalEdge); } } @Override protected String getResizeUpdateMessage(ResizeState state, INode child, INode parent, Rect newBounds, SegmentType horizontalEdge, SegmentType verticalEdge) { LinearResizeState resizeState = (LinearResizeState) state; updateResizeState(resizeState, child, parent, child.getBounds(), newBounds, horizontalEdge, verticalEdge); if (resizeState.useWeight) { String weight = formatFloatAttribute(resizeState.mWeight); String dimension = String.format("weight %1$s", weight); String width; String height; if (isVertical(parent)) { width = resizeState.getWidthAttribute(); height = dimension; } else { width = dimension; height = resizeState.getHeightAttribute(); } if (horizontalEdge == null) { return width; } else if (verticalEdge == null) { return height; } else { // U+00D7: Unicode for multiplication sign return String.format("%s \u00D7 %s", width, height); } } else { return super.getResizeUpdateMessage(state, child, parent, newBounds, horizontalEdge, verticalEdge); } } /** * Returns the layout weight of of the given child of a LinearLayout, or 0.0 if it * does not define a weight */ private static float getWeight(INode linearLayoutChild) { String weight = linearLayoutChild.getStringAttr(ANDROID_URI, ATTR_LAYOUT_WEIGHT); if (weight != null && weight.length() > 0) { try { return Float.parseFloat(weight); } catch (NumberFormatException nfe) { AdtPlugin.log(nfe, "Invalid weight %1$s", weight); } } return 0.0f; } /** * Returns the sum of all the layout weights of the children in the given LinearLayout * * @param linearLayout the layout to compute the total sum for * @return the total sum of all the layout weights in the given layout */ private static float getWeightSum(INode linearLayout) { String weightSum = linearLayout.getStringAttr(ANDROID_URI, ATTR_WEIGHT_SUM); float sum = -1.0f; if (weightSum != null) { // Distribute try { sum = Float.parseFloat(weightSum); return sum; } catch (NumberFormatException nfe) { // Just keep using the default } } return getSumOfWeights(linearLayout); } private static float getSumOfWeights(INode linearLayout) { float sum = 0.0f; for (INode child : linearLayout.getChildren()) { sum += getWeight(child); } return sum; } }