aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/LinearLayoutRule.java
diff options
context:
space:
mode:
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.java1092
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;
+ }
+}