| /* |
| * Copyright (C) 2015 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.uibuilder.handlers; |
| |
| import com.android.annotations.NonNull; |
| import com.android.annotations.Nullable; |
| import com.android.tools.idea.rendering.RenderTask; |
| import com.android.tools.idea.uibuilder.api.*; |
| import com.android.tools.idea.uibuilder.graphics.NlDrawingStyle; |
| import com.android.tools.idea.uibuilder.graphics.NlGraphics; |
| import com.android.tools.idea.uibuilder.model.*; |
| import com.android.tools.idea.uibuilder.model.Insets; |
| import com.android.tools.idea.uibuilder.surface.ScreenView; |
| import com.intellij.psi.xml.XmlTag; |
| |
| import java.awt.*; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Map; |
| |
| import static com.android.SdkConstants.*; |
| import static com.android.tools.idea.uibuilder.model.Coordinates.getSwingX; |
| import static com.android.tools.idea.uibuilder.model.Coordinates.getSwingY; |
| import static com.android.utils.XmlUtils.formatFloatAttribute; |
| |
| /** Handler for the {@code <LinearLayout>} layout */ |
| public class LinearLayoutHandler extends ViewGroupHandler { |
| @Override |
| public boolean paintConstraints(@NonNull ScreenView screenView, @NonNull Graphics2D graphics, @NonNull NlComponent component) { |
| NlComponent prev = null; |
| boolean vertical = isVertical(component); |
| for (NlComponent child : component.getChildren()) { |
| if (prev != null) { |
| if (vertical) { |
| int middle = getSwingY(screenView, (prev.y + prev.h + child.y) / 2); |
| NlGraphics.drawLine(NlDrawingStyle.GUIDELINE_DASHED, graphics, getSwingX(screenView, component.x), middle, |
| getSwingX(screenView, component.x + component.w), middle); |
| } else { |
| int middle = getSwingX(screenView, (prev.x + prev.w + child.x) / 2); |
| NlGraphics.drawLine(NlDrawingStyle.GUIDELINE_DASHED, graphics, middle, getSwingY(screenView, component.y), middle, |
| getSwingY(screenView, component.y + component.h)); |
| } |
| } |
| prev = child; |
| } |
| return false; |
| } |
| |
| /** |
| * Returns true if the given node represents a vertical linear layout. |
| * @param component the node to check layout orientation for |
| * @return true if the layout is in vertical mode, otherwise false |
| */ |
| protected boolean isVertical(@NonNull NlComponent component) { |
| // Horizontal is the default, so if no value is specified it is horizontal. |
| String orientation = component.getAttribute(ANDROID_URI, ATTR_ORIENTATION); |
| return VALUE_VERTICAL.equals(orientation); |
| } |
| |
| /** |
| * Returns the current orientation, regardless of whether it has been defined in XML |
| * |
| * @param component The LinearLayout to look up the orientation for |
| * @return "horizontal" or "vertical" depending on the current orientation of the |
| * linear layout |
| */ |
| private static String getCurrentOrientation(@NonNull final NlComponent component) { |
| String orientation = component.getAttribute(ANDROID_URI, ATTR_ORIENTATION); |
| if (orientation == null || orientation.length() == 0) { |
| orientation = VALUE_HORIZONTAL; |
| } |
| return orientation; |
| } |
| |
| private void distributeWeights(NlComponent parentNode, NlComponent[] targets) { |
| // Any XML to get weight sum? |
| String weightSum = parentNode.getAttribute(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 (NlComponent 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.getAttribute(ANDROID_URI, sizeAttribute))) { |
| target.setAttribute(ANDROID_URI, sizeAttribute, VALUE_ZERO_DP); |
| } |
| } |
| } |
| |
| private void clearWeights(NlComponent parentNode) { |
| // Clear attributes |
| String sizeAttribute = isVertical(parentNode) |
| ? ATTR_LAYOUT_HEIGHT : ATTR_LAYOUT_WIDTH; |
| for (NlComponent target : parentNode.getChildren()) { |
| target.setAttribute(ANDROID_URI, ATTR_LAYOUT_WEIGHT, null); |
| String size = target.getAttribute(ANDROID_URI, sizeAttribute); |
| if (size != null && size.startsWith("0")) { //$NON-NLS-1$ |
| target.setAttribute(ANDROID_URI, sizeAttribute, VALUE_WRAP_CONTENT); |
| } |
| } |
| } |
| |
| @Override |
| @Nullable |
| public DragHandler createDragHandler(@NonNull ViewEditor editor, |
| @NonNull NlComponent layout, |
| @NonNull List<NlComponent> components, |
| @NonNull DragType type) { |
| if (layout.w == 0 || layout.h == 0) { |
| return null; |
| } |
| return new LinearDragHandler(editor, layout, components, type); |
| } |
| |
| private class LinearDragHandler extends DragHandler { |
| /** |
| * Vertical layout? |
| */ |
| private final boolean myVertical; |
| |
| /** |
| * Insert points (pixels + index) |
| */ |
| private final List<MatchPos> myIndices; |
| |
| /** |
| * Number of insert positions in the target node |
| */ |
| private final int myNumPositions; |
| |
| /** |
| * Current marker X position |
| */ |
| private Integer myCurrX; |
| |
| /** |
| * Current marker Y position |
| */ |
| private Integer myCurrY; |
| |
| /** |
| * Position of the dragged element in this layout (or |
| * -1 if the dragged element is from elsewhere) |
| */ |
| private int mySelfPos; |
| |
| /** |
| * Current drop insert index (-1 for "at the end") |
| */ |
| private int myInsertPos = -1; |
| |
| /** |
| * width of match line if it's a horizontal one |
| */ |
| private Integer myWidth; |
| |
| /** |
| * height of match line if it's a vertical one |
| */ |
| private Integer myHeight; |
| |
| |
| public LinearDragHandler(@NonNull ViewEditor editor, |
| @NonNull NlComponent layout, |
| @NonNull List<NlComponent> components, |
| @NonNull DragType type) { |
| super(editor, LinearLayoutHandler.this, layout, components, type); |
| assert !components.isEmpty(); |
| |
| myVertical = isVertical(layout); |
| |
| // Prepare a list of insertion points: X coordinates for horizontal, Y for |
| // vertical. |
| myIndices = new ArrayList<MatchPos>(); |
| |
| int last = myVertical ? layout.y + layout.getPadding().top : layout.x + layout.getPadding().left; |
| int pos = 0; |
| boolean lastDragged = false; |
| mySelfPos = -1; |
| for (NlComponent it : layout.getChildren()) { |
| if (it.w > 0 && it.h > 0) { |
| boolean isDragged = components.contains(it); |
| |
| // 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 = myVertical ? it.y + (it.h / 2) : it.x + (it.w / 2); |
| mySelfPos = pos; |
| myIndices.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 = myVertical ? it.y : it.x; |
| v = (last + v) / 2; |
| myIndices.add(new MatchPos(v, pos++)); |
| } |
| |
| last = myVertical ? (it.y + it.h) : (it.x + it.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; |
| myIndices.add(new MatchPos(v, pos)); |
| } |
| |
| myNumPositions = layout.getChildCount() + 1; |
| } |
| |
| @Nullable |
| @Override |
| public String update(@AndroidCoordinate int x, @AndroidCoordinate int y, int modifiers) { |
| super.update(x, y, modifiers); |
| |
| boolean isVertical = myVertical; |
| |
| int bestDist = Integer.MAX_VALUE; |
| int bestIndex = Integer.MIN_VALUE; |
| Integer bestPos = null; |
| |
| for (MatchPos index : myIndices) { |
| int i = index.getDistance(); |
| int pos = index.getPosition(); |
| int dist = (isVertical ? y : 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) { |
| if (isVertical) { |
| myCurrX = layout.x + layout.w / 2; |
| myCurrY = bestIndex; |
| myWidth = layout.w; |
| myHeight = null; |
| } |
| else { |
| myCurrX = bestIndex; |
| myCurrY = layout.y + layout.h / 2; |
| myWidth = null; |
| myHeight = layout.h; |
| } |
| |
| myInsertPos = bestPos; |
| } |
| |
| return null; |
| } |
| |
| @Override |
| public void paint(@NonNull NlGraphics gc) { |
| Insets padding = layout.getPadding(); |
| int layoutX = layout.x + padding.left; |
| int layoutW = layout.w - padding.width(); |
| int layoutY = layout.y + padding.top; |
| int layoutH = layout.h - padding.height(); |
| |
| // Highlight the receiver |
| gc.useStyle(NlDrawingStyle.DROP_RECIPIENT); |
| gc.drawRect(layoutX, layoutY, layoutW, layoutH); |
| |
| gc.useStyle(NlDrawingStyle.DROP_ZONE); |
| |
| boolean isVertical = myVertical; |
| int selfPos = mySelfPos; |
| |
| for (MatchPos it : myIndices) { |
| 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(layoutX, i, layoutW, i); |
| } |
| else { |
| // draw vertical lines |
| gc.drawLine(i, layoutY, i, layoutH); |
| } |
| } |
| } |
| |
| Integer currX = myCurrX; |
| Integer currY = myCurrY; |
| |
| if (currX != null && currY != null) { |
| gc.useStyle(NlDrawingStyle.DROP_ZONE_ACTIVE); |
| |
| int x = currX; |
| int y = currY; |
| |
| NlComponent be = components.get(0); |
| |
| // Draw a clear line at the closest drop zone (unless we're over the |
| // dragged element itself) |
| if (myInsertPos != selfPos || selfPos == -1) { |
| gc.useStyle(NlDrawingStyle.DROP_PREVIEW); |
| if (myWidth != null) { |
| int width = myWidth; |
| int fromX = x - width / 2; |
| int toX = x + width / 2; |
| gc.drawLine(fromX, y, toX, y); |
| } |
| else if (myHeight != null) { |
| int height = myHeight; |
| int fromY = y - height / 2; |
| int toY = y + height / 2; |
| gc.drawLine(x, fromY, x, toY); |
| } |
| } |
| |
| if (be.w > 0 && be.h > 0) { |
| boolean isLast = myInsertPos == myNumPositions - 1; |
| |
| // 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 = layoutX - be.x; |
| offsetY = currY - be.y - (isLast ? 0 : (be.h / 2)); |
| |
| } |
| else { |
| offsetX = currX - be.x - (isLast ? 0 : (be.w / 2)); |
| offsetY = layoutY - be.y; |
| } |
| |
| gc.useStyle(NlDrawingStyle.DROP_PREVIEW); |
| for (NlComponent element : components) { |
| if (element.w > 0 && element.h > 0 && (element.w > layoutW || element.h > layoutH) && |
| layout.getChildCount() == 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 (element.w > layoutW) { |
| px = layoutX; |
| pw = layoutW; |
| } |
| else { |
| px = element.x + offsetX; |
| pw = element.w; |
| } |
| if (element.h > layoutH) { |
| py = layoutY; |
| ph = layoutH; |
| } |
| else { |
| py = element.y + offsetY; |
| ph = element.h; |
| } |
| gc.drawRect(px, py, pw, ph); |
| } |
| else { |
| drawElement(gc, element, offsetX, offsetY); |
| } |
| } |
| } |
| } |
| } |
| |
| /** |
| * Draws the bounds of the given elements and all its children elements in the canvas |
| * with the specified offset. |
| * |
| * @param gc the graphics context |
| * @param component the element to be drawn |
| * @param offsetX a horizontal delta to add to the current bounds of the element when |
| * drawing it |
| * @param offsetY a vertical delta to add to the current bounds of the element when |
| * drawing it |
| */ |
| public void drawElement(NlGraphics gc, NlComponent component, int offsetX, int offsetY) { |
| if (component.w > 0 && component.h > 0) { |
| gc.drawRect(component.x + offsetX, component.y + offsetY, component.w, component.h); |
| } |
| |
| for (NlComponent inner : component.getChildren()) { |
| drawElement(gc, inner, offsetX, offsetY); |
| } |
| } |
| |
| @Override |
| public int getInsertIndex() { |
| return myInsertPos; |
| } |
| |
| @Override |
| public void commit(@AndroidCoordinate int x, @AndroidCoordinate int y, int modifiers) { |
| } |
| } |
| |
| /** A possible match position */ |
| private static class MatchPos { |
| /** The pixel distance */ |
| private final int myDistance; |
| /** The position among siblings */ |
| private final int myPosition; |
| |
| public MatchPos(int distance, int position) { |
| myDistance = distance; |
| myPosition = position; |
| } |
| |
| private int getDistance() { |
| return myDistance; |
| } |
| |
| private int getPosition() { |
| return myPosition; |
| } |
| } |
| |
| @Override |
| public void onChildInserted(@NonNull NlComponent layout, @NonNull NlComponent newChild, @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. |
| ViewHandler viewHandler = newChild.getViewHandler(); |
| if (viewHandler != null) { |
| boolean vertical = isVertical(layout); |
| FillPolicy fill = viewHandler.getFillPolicy(); |
| String fillParent = VALUE_MATCH_PARENT; |
| if (fill.fillHorizontally(vertical)) { |
| newChild.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, fillParent); |
| } else if (!vertical && fill == FillPolicy.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) |
| newChild.setAttribute(ANDROID_URI, ATTR_LAYOUT_WEIGHT, VALUE_1); |
| } |
| if (fill.fillVertically(vertical)) { |
| newChild.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(layout) ? ATTR_LAYOUT_HEIGHT : ATTR_LAYOUT_WIDTH; |
| for (NlComponent target : layout.getChildren()) { |
| if (target == newChild) { |
| continue; |
| } |
| String weight = target.getAttribute(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.getAttribute(ANDROID_URI, sizeAttribute); |
| if (size != null && !size.startsWith("0")) { //$NON-NLS-1$ |
| duplicate0dip = false; |
| break; |
| } |
| } |
| if (duplicateWeight && sameWeight != null) { |
| newChild.setAttribute(ANDROID_URI, ATTR_LAYOUT_WEIGHT, sameWeight); |
| if (duplicate0dip) { |
| newChild.setAttribute(ANDROID_URI, sizeAttribute, VALUE_ZERO_DP); |
| } |
| } |
| } |
| |
| @Override |
| @Nullable |
| public ResizeHandler createResizeHandler(@NonNull ViewEditor editor, |
| @NonNull NlComponent component, |
| @Nullable SegmentType horizontalEdgeType, |
| @Nullable SegmentType verticalEdgeType) { |
| return new LinearResizeHandler(editor, this, component, horizontalEdgeType, verticalEdgeType); |
| } |
| |
| private class LinearResizeHandler extends DefaultResizeHandler { |
| /** 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<NlComponent, Dimension> 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<NlComponent> mClearWeights; |
| |
| public LinearResizeHandler(@NonNull ViewEditor editor, |
| @NonNull ViewGroupHandler handler, |
| @NonNull NlComponent component, |
| @Nullable SegmentType horizontalEdgeType, |
| @Nullable SegmentType verticalEdgeType) { |
| super(editor, handler, component, horizontalEdgeType, verticalEdgeType); |
| |
| unweightedSizes = editor.measureChildren(layout, new RenderTask.AttributeFilter() { |
| @Override |
| public String getAttribute(@NonNull XmlTag 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) && ANDROID_URI.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); |
| if (unweightedSizes != null) { |
| for (Map.Entry<NlComponent, Dimension> entry : unweightedSizes.entrySet()) { |
| Dimension preferredSize = entry.getValue(); |
| if (isVertical) { |
| totalLength += preferredSize.height; |
| } |
| else { |
| totalLength += preferredSize.width; |
| } |
| } |
| } |
| } |
| |
| /** 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(NlComponent n) { |
| if (mClearWeights == null) { |
| mClearWeights = new ArrayList<NlComponent>(); |
| } |
| mClearWeights.add(n); |
| } |
| |
| /** Applies the state to the nodes */ |
| public void apply() { |
| assert useWeight; |
| |
| String value = mWeight > 0 ? formatFloatAttribute(mWeight) : null; |
| component.setAttribute(ANDROID_URI, ATTR_LAYOUT_WEIGHT, value); |
| |
| if (mClearWeights != null) { |
| for (NlComponent 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)); |
| } |
| } |
| |
| protected void updateResizeState(final NlComponent component, NlComponent 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. |
| 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 (wrapWidth || fillWidth) { |
| clearWeight(component); |
| return; |
| } |
| if (newBounds.width == oldBounds.width) { |
| return; |
| } |
| } |
| |
| if (isVertical && horizontalEdge != null) { |
| if (wrapHeight || fillHeight) { |
| clearWeight(component); |
| return; |
| } |
| if (newBounds.height == oldBounds.height) { |
| return; |
| } |
| } |
| |
| // Compute weight sum |
| float sum = getWeightSum(layout); |
| if (sum <= 0.0f) { |
| sum = 1.0f; |
| 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<NlComponent, Dimension> sizes = unweightedSizes; |
| Dimension nodePreferredSize = sizes != null ? sizes.get(component) : null; |
| if (nodePreferredSize != null) { |
| if (horizontalEdge != null && newBounds.height < nodePreferredSize.height || |
| verticalEdge != null && newBounds.width < nodePreferredSize.width) { |
| return; |
| } |
| } |
| |
| Rectangle layoutBounds = new Rectangle(layout.x, layout.y, layout.w, layout.h); |
| int remaining = (isVertical ? layoutBounds.height : layoutBounds.width) - totalLength; |
| Dimension nodeBounds = sizes != null ? sizes.get(component) : null; |
| if (nodeBounds == null) { |
| return; |
| } |
| |
| if (remaining > 0) { |
| int missing = 0; |
| if (isVertical) { |
| if (newBounds.height > nodeBounds.height) { |
| missing = newBounds.height - nodeBounds.height; |
| } else if (wrapBounds != null && newBounds.height > wrapBounds.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 - wrapBounds.height; |
| remaining += nodeBounds.height - wrapBounds.height; |
| wrapHeight = true; |
| } |
| } else { |
| if (newBounds.width > nodeBounds.width) { |
| missing = newBounds.width - nodeBounds.width; |
| } else if (wrapBounds != null && newBounds.width > wrapBounds.width) { |
| missing = newBounds.width - wrapBounds.width; |
| remaining += nodeBounds.width - wrapBounds.width; |
| wrapWidth = true; |
| } |
| } |
| if (missing > 0) { |
| // (weight / weightSum) * remaining = missing, so |
| // weight = missing * weightSum / remaining |
| float weight = missing * sum / remaining; |
| 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(@NonNull NlComponent component, |
| @NonNull NlComponent layout, |
| @NonNull Rectangle oldBounds, |
| @NonNull Rectangle newBounds, |
| @Nullable SegmentType horizontalEdge, |
| @Nullable SegmentType verticalEdge) { |
| updateResizeState(component, layout, oldBounds, newBounds, horizontalEdge, verticalEdge); |
| |
| if (useWeight) { |
| apply(); |
| |
| // Handle resizing in the opposite dimension of the layout |
| final boolean isVertical = isVertical(layout); |
| if (!isVertical && horizontalEdge != null) { |
| if (newBounds.height != oldBounds.height || wrapHeight || fillHeight) { |
| component.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT, getHeightAttribute()); |
| } |
| } |
| if (isVertical && verticalEdge != null) { |
| if (newBounds.width != oldBounds.width || wrapWidth || fillWidth) { |
| component.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, getWidthAttribute()); |
| } |
| } |
| } |
| else { |
| component.setAttribute(ANDROID_URI, ATTR_LAYOUT_WEIGHT, null); |
| super.setNewSizeBounds(component, layout, oldBounds, newBounds, horizontalEdge, verticalEdge); |
| } |
| } |
| |
| @Override |
| protected String getResizeUpdateMessage(@NonNull NlComponent child, |
| @NonNull NlComponent parent, |
| @NonNull Rectangle newBounds, |
| @Nullable SegmentType horizontalEdge, |
| @Nullable SegmentType verticalEdge) { |
| updateResizeState(child, parent, newBounds, newBounds, |
| horizontalEdge, verticalEdge); |
| |
| if (useWeight) { |
| String weight = formatFloatAttribute(mWeight); |
| String dimension = String.format("weight %1$s", weight); |
| |
| String width; |
| String height; |
| if (isVertical(parent)) { |
| width = getWidthAttribute(); |
| height = dimension; |
| } else { |
| width = dimension; |
| height = 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(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(@NonNull NlComponent linearLayoutChild) { |
| String weight = linearLayoutChild.getAttribute(ANDROID_URI, ATTR_LAYOUT_WEIGHT); |
| if (weight != null && weight.length() > 0) { |
| try { |
| return Float.parseFloat(weight); |
| } catch (NumberFormatException ignore) { |
| } |
| } |
| |
| 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(@NonNull NlComponent linearLayout) { |
| String weightSum = linearLayout.getAttribute(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(@NonNull NlComponent linearLayout) { |
| float sum = 0.0f; |
| for (NlComponent child : linearLayout.getChildren()) { |
| sum += getWeight(child); |
| } |
| |
| return sum; |
| } |
| } |