| /* |
| * 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; |
| } |
| } |