/* * 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.RenderService; 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) { RenderService service = AndroidDesignerUtils.getRenderService(myContext.getArea()); if (service == null) { return null; } // Measure unweighted bounds Map<XmlTag, ViewInfo> map = service.measureChildren(parentTag, new RenderService.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)); } } } }