diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/LinearLayoutRule.java')
-rw-r--r-- | eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/LinearLayoutRule.java | 1092 |
1 files changed, 1092 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/LinearLayoutRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/LinearLayoutRule.java new file mode 100644 index 000000000..610fe5d8b --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/LinearLayoutRule.java @@ -0,0 +1,1092 @@ +/* + * 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; + } +} |