blob: 610fe5d8b033198f36855a596eecac08083561ab [file] [log] [blame]
/*
* 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;
}
}