blob: 66c47c308786d663bfadab640959a6148d3f6c1e [file] [log] [blame]
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
*
* 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.tools.idea.designer;
import com.android.ide.common.rendering.api.ViewInfo;
import com.android.tools.idea.rendering.RenderTask;
import com.android.utils.XmlUtils;
import com.google.common.collect.Maps;
import com.intellij.android.designer.AndroidDesignerUtils;
import com.intellij.android.designer.model.RadViewComponent;
import com.intellij.designer.designSurface.EditableArea;
import com.intellij.designer.designSurface.OperationContext;
import com.intellij.designer.model.RadComponent;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.psi.xml.XmlTag;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.awt.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static com.android.SdkConstants.*;
import static com.intellij.android.designer.designSurface.graphics.DrawingStyle.MAX_MATCH_DISTANCE;
public class LinearLayoutResizeOperation extends ResizeOperation {
public LinearLayoutResizeOperation(OperationContext context) {
super(context);
}
/**
* 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 static boolean isVertical(@NotNull RadViewComponent node) {
// Horizontal is the default, so if no value is specified it is horizontal.
@NotNull XmlTag tag = node.getTag();
return VALUE_VERTICAL.equals(tag.getAttributeValue(ATTR_ORIENTATION, ANDROID_URI));
}
protected void updateResizeContext(LinearResizeContext resizeContext,
final RadViewComponent node,
RadViewComponent layout,
Rectangle oldBounds,
Rectangle 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.
resizeContext.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 (resizeContext.wrapWidth || resizeContext.fillWidth) {
resizeContext.clearWeight(node);
return;
}
if (newBounds.width == oldBounds.width) {
return;
}
}
if (isVertical && horizontalEdge != null) {
if (resizeContext.wrapHeight || resizeContext.fillHeight) {
resizeContext.clearWeight(node);
return;
}
if (newBounds.height == oldBounds.height) {
return;
}
}
// Compute weight sum
float sum = getWeightSum(layout);
if (sum <= 0.0f) {
sum = 1.0f;
resizeContext.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<XmlTag, Dimension> sizes = resizeContext.myUnweightedSizes;
Dimension nodePreferredSize = sizes.get(node.getTag());
if (nodePreferredSize != null) {
if (horizontalEdge != null && newBounds.height < nodePreferredSize.height ||
verticalEdge != null && newBounds.width < nodePreferredSize.width) {
return;
}
}
Rectangle layoutBounds = layout.getBounds();
int remaining = (isVertical ? layoutBounds.height : layoutBounds.width) - resizeContext.myTotalLength;
Dimension nodeSize = sizes.get(node.getTag());
if (nodeSize == null) {
return;
}
if (remaining > 0) {
int missing = 0;
if (isVertical) {
if (newBounds.height > nodeSize.height) {
missing = newBounds.height - nodeSize.height;
} else if (newBounds.height > resizeContext.wrapSize.height) {
// 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.height - resizeContext.wrapSize.height;
remaining += nodeSize.height - resizeContext.wrapSize.height;
resizeContext.wrapHeight = true;
}
} else {
if (newBounds.width > nodeSize.width) {
missing = newBounds.width - nodeSize.width;
} else if (newBounds.width > resizeContext.wrapSize.width) {
missing = newBounds.width - resizeContext.wrapSize.width;
remaining += nodeSize.width - resizeContext.wrapSize.width;
resizeContext.wrapWidth = true;
}
}
if (missing > 0) {
// (weight / weightSum) * remaining = missing, so
// weight = missing * weightSum / remaining
float weight = missing * sum / remaining;
resizeContext.setWeight(weight);
}
}
}
@Override
public void onResizeUpdate(@NotNull RadViewComponent parent, @NotNull Rectangle newBounds, int modifierMask) {
myResizeContext.bounds = newBounds;
myResizeContext.modifierMask = modifierMask;
// Match on wrap bounds
myResizeContext.wrapWidth = myResizeContext.wrapHeight = false;
if (myResizeContext.wrapSize != null) {
Dimension b = myResizeContext.wrapSize;
int maxMatchDistance = MAX_MATCH_DISTANCE;
if (myResizeContext.horizontalEdgeType != null) {
if (Math.abs(newBounds.height - b.height) < maxMatchDistance) {
myResizeContext.wrapHeight = true;
if (myResizeContext.horizontalEdgeType == SegmentType.TOP) {
newBounds.y += newBounds.height - b.height;
}
newBounds.height = b.height;
}
}
if (myResizeContext.verticalEdgeType != null) {
if (Math.abs(newBounds.width - b.width) < maxMatchDistance) {
myResizeContext.wrapWidth = true;
if (myResizeContext.verticalEdgeType == SegmentType.LEFT) {
newBounds.x += newBounds.width - b.width;
}
newBounds.width = b.width;
}
}
}
// Match on fill bounds
myResizeContext.horizontalFillSegment = null;
myResizeContext.fillHeight = false;
if (myResizeContext.horizontalEdgeType == SegmentType.BOTTOM && !myResizeContext.wrapHeight) {
Rectangle parentBounds = parent.getBounds();
myResizeContext.horizontalFillSegment = new Segment(parentBounds.y + parentBounds.height, newBounds.x, newBounds.x + newBounds.width,
null /*node*/, null /*id*/, SegmentType.BOTTOM, MarginType.NO_MARGIN);
if (Math.abs(newBounds.y + newBounds.height - (parentBounds.y + parentBounds.height)) < MAX_MATCH_DISTANCE) {
myResizeContext.fillHeight = true;
newBounds.height = parentBounds.y + parentBounds.height - newBounds.y;
}
}
myResizeContext.verticalFillSegment = null;
myResizeContext.fillWidth = false;
if (myResizeContext.verticalEdgeType == SegmentType.RIGHT && !myResizeContext.wrapWidth) {
Rectangle parentBounds = parent.getBounds();
myResizeContext.verticalFillSegment = new Segment(parentBounds.x + parentBounds.width, newBounds.y, newBounds.y + newBounds.height,
null /*node*/, null /*id*/, SegmentType.RIGHT, MarginType.NO_MARGIN);
if (Math.abs(newBounds.x + newBounds.width - (parentBounds.x + parentBounds.width)) < MAX_MATCH_DISTANCE) {
myResizeContext.fillWidth = true;
newBounds.width = parentBounds.x + parentBounds.width - newBounds.x;
}
}
}
/**
* {@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(ResizeContext context, final RadViewComponent node, RadViewComponent layout,
Rectangle oldBounds, Rectangle newBounds, SegmentType horizontalEdge,
SegmentType verticalEdge) {
LinearResizeContext resizeContext = (LinearResizeContext) context;
updateResizeContext(resizeContext, node, layout, oldBounds, newBounds, horizontalEdge, verticalEdge);
if (resizeContext.myUseWeight) {
// Handle resizing in the opposite dimension of the layout
final boolean isVertical = isVertical(layout);
if (isVertical && horizontalEdge != null || !isVertical && verticalEdge != null) {
resizeContext.apply();
}
if (!isVertical && horizontalEdge != null) {
if (newBounds.height != oldBounds.height || resizeContext.wrapHeight || resizeContext.fillHeight) {
node.setAttribute(ATTR_LAYOUT_HEIGHT, ANDROID_URI, resizeContext.getHeightAttribute());
}
}
if (isVertical && verticalEdge != null) {
if (newBounds.width != oldBounds.width || resizeContext.wrapWidth || resizeContext.fillWidth) {
node.setAttribute(ATTR_LAYOUT_WIDTH, ANDROID_URI, resizeContext.getWidthAttribute());
}
}
} else {
node.setAttribute(ATTR_LAYOUT_WEIGHT, ANDROID_URI, null);
super.setNewSizeBounds(resizeContext, node, layout, oldBounds, newBounds, horizontalEdge, verticalEdge);
}
}
@Override
protected String getResizeUpdateMessage(ResizeContext context, RadViewComponent child, RadViewComponent parent,
Rectangle newBounds, SegmentType horizontalEdge, SegmentType verticalEdge) {
LinearResizeContext resizeContext = (LinearResizeContext) context;
updateResizeContext(resizeContext, child, parent, child.getBounds(), newBounds, horizontalEdge, verticalEdge);
if (resizeContext.myUseWeight) {
String weight = XmlUtils.formatFloatAttribute(resizeContext.myWeight);
String dimension = String.format("weight %1$s", weight);
String width;
String height;
if (isVertical(parent)) {
width = resizeContext.getWidthAttribute();
height = dimension;
} else {
width = dimension;
height = resizeContext.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(context, child, parent, newBounds, horizontalEdge, verticalEdge);
}
}
/**
* Returns the layout weight off the given child of a LinearLayout, or 0.0 if it
* does not define a weight
*/
private static float getWeight(RadViewComponent linearLayoutChild) {
String weight = linearLayoutChild.getTag().getAttributeValue(ATTR_LAYOUT_WEIGHT, ANDROID_URI);
if (weight != null && weight.length() > 0) {
try {
return Float.parseFloat(weight);
} catch (NumberFormatException nfe) {
Logger.getInstance(LinearLayoutResizeOperation.class).warn(String.format("Invalid weight %1$s", weight), nfe);
}
}
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(RadViewComponent linearLayout) {
String weightSum = linearLayout.getTag().getAttributeValue(ATTR_WEIGHT_SUM, ANDROID_URI);
if (weightSum != null) {
// Distribute
try {
return Float.parseFloat(weightSum);
} catch (NumberFormatException nfe) {
// Just keep using the default
}
}
return getSumOfWeights(linearLayout);
}
private static float getSumOfWeights(RadViewComponent linearLayout) {
float sum = 0.0f;
for (RadComponent child : linearLayout.getChildren()) {
sum += getWeight((RadViewComponent)child);
}
return sum;
}
@Override
protected ResizeContext createResizeContext(RadViewComponent layout, @Nullable Object layoutView, RadViewComponent node) {
return new LinearResizeContext(myContext.getArea(), layout, layoutView, node);
}
/** Custom resize state used during linear layout resizing */
private class LinearResizeContext extends ResizeContext {
/** Whether the node should be assigned a new weight */
public boolean myUseWeight;
/** Weight sum to be applied to the parent */
private float myNewWeightSum;
/** The weight to be set on the node (provided {@link #myUseWeight} is true) */
private float myWeight;
/** Map from nodes to preferred bounds of nodes where the weights have been cleared */
public final Map<XmlTag, Dimension> myUnweightedSizes;
/** Total required size required by the siblings <b>without</b> weights */
public int myTotalLength;
/** List of nodes which should have their weights cleared */
public List<RadViewComponent> myClearWeights;
private LinearResizeContext(EditableArea area, RadViewComponent layout, @Nullable Object layoutView, RadViewComponent node) {
super(area, layout, layoutView, node);
myUnweightedSizes = computeUnweightedSizes();
if (myUnweightedSizes != null) {
// Compute total required size required by the siblings *without* weights
myTotalLength = 0;
final boolean isVertical = isVertical(layout);
for (Map.Entry<XmlTag, Dimension> entry : myUnweightedSizes.entrySet()) {
Dimension preferredSize = entry.getValue();
if (isVertical) {
myTotalLength += preferredSize.height;
} else {
myTotalLength += preferredSize.width;
}
}
}
}
@Nullable
private Map<XmlTag, Dimension> computeUnweightedSizes() {
Map<XmlTag, Dimension> unweightedSizes = Maps.newHashMap();
RadComponent parent = myComponent.getParent();
if (!(parent instanceof RadViewComponent)) {
return null;
}
XmlTag parentTag = ((RadViewComponent)parent).getTag();
if (parentTag != null) {
RenderTask task = AndroidDesignerUtils.createRenderTask(myContext.getArea());
if (task == null) {
return null;
}
// Measure unweighted bounds
Map<XmlTag, ViewInfo> map = task.measureChildren(parentTag, new RenderTask.AttributeFilter() {
@Override
public String getAttribute(@NotNull XmlTag n, @Nullable String namespace, @NotNull String localName) {
// Clear out layout weights; we need to measure the unweighted sizes
// of the children
if (ATTR_LAYOUT_WEIGHT.equals(localName) && ANDROID_URI.equals(namespace)) {
return ""; //$NON-NLS-1$
}
return null;
}
});
if (map != null) {
for (Map.Entry<XmlTag, ViewInfo> entry : map.entrySet()) {
ViewInfo viewInfo = entry.getValue();
Dimension size = new Dimension(viewInfo.getRight() - viewInfo.getLeft(), viewInfo.getBottom() - viewInfo.getTop());
unweightedSizes.put(entry.getKey(), size);
}
}
}
return unweightedSizes;
}
/** Resets the computed state */
void reset() {
myNewWeightSum = -1;
myUseWeight = false;
myClearWeights = null;
}
/** Sets a weight to be applied to the node */
void setWeight(float weight) {
myUseWeight = true;
this.myWeight = weight;
}
/** Sets a weight sum to be applied to the parent layout */
void setWeightSum(float weightSum) {
myNewWeightSum = weightSum;
}
/** Marks that the given node should be cleared when applying the new size */
void clearWeight(RadViewComponent n) {
if (myClearWeights == null) {
myClearWeights = new ArrayList<RadViewComponent>();
}
myClearWeights.add(n);
}
/** Applies the state to the nodes */
public void apply() {
assert myUseWeight;
String value = myWeight > 0 ? XmlUtils.formatFloatAttribute(myWeight) : null;
node.setAttribute(ATTR_LAYOUT_WEIGHT, ANDROID_URI, value);
if (myClearWeights != null) {
for (RadViewComponent n : myClearWeights) {
if (getWeight(n) > 0.0f) {
n.setAttribute(ATTR_LAYOUT_WEIGHT, ANDROID_URI, null);
}
}
}
if (myNewWeightSum > 0.0) {
layout.setAttribute(ATTR_WEIGHT_SUM, ANDROID_URI, XmlUtils.formatFloatAttribute(myNewWeightSum));
}
}
}
}