Home | History | Annotate | Download | only in layout
      1 /*
      2  * Copyright (C) 2010 The Android Open Source Project
      3  *
      4  * Licensed under the Eclipse Public License, Version 1.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.eclipse.org/org/documents/epl-v10.php
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.ide.common.layout;
     18 
     19 import static com.android.SdkConstants.ANDROID_URI;
     20 import static com.android.SdkConstants.ATTR_BASELINE_ALIGNED;
     21 import static com.android.SdkConstants.ATTR_LAYOUT_GRAVITY;
     22 import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
     23 import static com.android.SdkConstants.ATTR_LAYOUT_WEIGHT;
     24 import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
     25 import static com.android.SdkConstants.ATTR_ORIENTATION;
     26 import static com.android.SdkConstants.ATTR_WEIGHT_SUM;
     27 import static com.android.SdkConstants.VALUE_1;
     28 import static com.android.SdkConstants.VALUE_HORIZONTAL;
     29 import static com.android.SdkConstants.VALUE_VERTICAL;
     30 import static com.android.SdkConstants.VALUE_WRAP_CONTENT;
     31 import static com.android.SdkConstants.VALUE_ZERO_DP;
     32 import static com.android.ide.eclipse.adt.AdtUtils.formatFloatAttribute;
     33 
     34 import com.android.SdkConstants;
     35 import com.android.annotations.NonNull;
     36 import com.android.annotations.Nullable;
     37 import com.android.ide.common.api.DrawingStyle;
     38 import com.android.ide.common.api.DropFeedback;
     39 import com.android.ide.common.api.IClientRulesEngine;
     40 import com.android.ide.common.api.IDragElement;
     41 import com.android.ide.common.api.IFeedbackPainter;
     42 import com.android.ide.common.api.IGraphics;
     43 import com.android.ide.common.api.IMenuCallback;
     44 import com.android.ide.common.api.INode;
     45 import com.android.ide.common.api.INodeHandler;
     46 import com.android.ide.common.api.IViewMetadata;
     47 import com.android.ide.common.api.IViewMetadata.FillPreference;
     48 import com.android.ide.common.api.IViewRule;
     49 import com.android.ide.common.api.InsertType;
     50 import com.android.ide.common.api.Point;
     51 import com.android.ide.common.api.Rect;
     52 import com.android.ide.common.api.RuleAction;
     53 import com.android.ide.common.api.RuleAction.Choices;
     54 import com.android.ide.common.api.SegmentType;
     55 import com.android.ide.eclipse.adt.AdtPlugin;
     56 
     57 import java.net.URL;
     58 import java.util.ArrayList;
     59 import java.util.Arrays;
     60 import java.util.Collections;
     61 import java.util.List;
     62 import java.util.Map;
     63 
     64 /**
     65  * An {@link IViewRule} for android.widget.LinearLayout and all its derived
     66  * classes.
     67  */
     68 public class LinearLayoutRule extends BaseLayoutRule {
     69     private static final String ACTION_ORIENTATION = "_orientation"; //$NON-NLS-1$
     70     private static final String ACTION_WEIGHT = "_weight"; //$NON-NLS-1$
     71     private static final String ACTION_DISTRIBUTE = "_distribute"; //$NON-NLS-1$
     72     private static final String ACTION_BASELINE = "_baseline"; //$NON-NLS-1$
     73     private static final String ACTION_CLEAR = "_clear"; //$NON-NLS-1$
     74     private static final String ACTION_DOMINATE = "_dominate"; //$NON-NLS-1$
     75 
     76     private static final URL ICON_HORIZONTAL =
     77         LinearLayoutRule.class.getResource("hlinear.png"); //$NON-NLS-1$
     78     private static final URL ICON_VERTICAL =
     79         LinearLayoutRule.class.getResource("vlinear.png"); //$NON-NLS-1$
     80     private static final URL ICON_WEIGHTS =
     81         LinearLayoutRule.class.getResource("weights.png"); //$NON-NLS-1$
     82     private static final URL ICON_DISTRIBUTE =
     83         LinearLayoutRule.class.getResource("distribute.png"); //$NON-NLS-1$
     84     private static final URL ICON_BASELINE =
     85         LinearLayoutRule.class.getResource("baseline.png"); //$NON-NLS-1$
     86     private static final URL ICON_CLEAR_WEIGHTS =
     87             LinearLayoutRule.class.getResource("clearweights.png"); //$NON-NLS-1$
     88     private static final URL ICON_DOMINATE =
     89             LinearLayoutRule.class.getResource("allweight.png"); //$NON-NLS-1$
     90 
     91     /**
     92      * Returns the current orientation, regardless of whether it has been defined in XML
     93      *
     94      * @param node The LinearLayout to look up the orientation for
     95      * @return "horizontal" or "vertical" depending on the current orientation of the
     96      *         linear layout
     97      */
     98     private String getCurrentOrientation(final INode node) {
     99         String orientation = node.getStringAttr(ANDROID_URI, ATTR_ORIENTATION);
    100         if (orientation == null || orientation.length() == 0) {
    101             orientation = VALUE_HORIZONTAL;
    102         }
    103         return orientation;
    104     }
    105 
    106     /**
    107      * Returns true if the given node represents a vertical linear layout.
    108      * @param node the node to check layout orientation for
    109      * @return true if the layout is in vertical mode, otherwise false
    110      */
    111     protected boolean isVertical(INode node) {
    112         // Horizontal is the default, so if no value is specified it is horizontal.
    113         return VALUE_VERTICAL.equals(node.getStringAttr(ANDROID_URI,
    114                 ATTR_ORIENTATION));
    115     }
    116 
    117     /**
    118      * Returns true if this LinearLayout supports switching orientation.
    119      *
    120      * @return true if this layout supports orientations
    121      */
    122     protected boolean supportsOrientation() {
    123         return true;
    124     }
    125 
    126     @Override
    127     public void addLayoutActions(
    128             @NonNull List<RuleAction> actions,
    129             final @NonNull INode parentNode,
    130             final @NonNull List<? extends INode> children) {
    131         super.addLayoutActions(actions, parentNode, children);
    132         if (supportsOrientation()) {
    133             Choices action = RuleAction.createChoices(
    134                     ACTION_ORIENTATION, "Orientation",  //$NON-NLS-1$
    135                     new PropertyCallback(Collections.singletonList(parentNode),
    136                             "Change LinearLayout Orientation",
    137                             ANDROID_URI, ATTR_ORIENTATION),
    138                     Arrays.<String>asList("Set Horizontal Orientation","Set Vertical Orientation"),
    139                     Arrays.<URL>asList(ICON_HORIZONTAL, ICON_VERTICAL),
    140                     Arrays.<String>asList("horizontal", "vertical"),
    141                     getCurrentOrientation(parentNode),
    142                     null /* icon */,
    143                     -10,
    144                     false /* supportsMultipleNodes */
    145             );
    146             action.setRadio(true);
    147             actions.add(action);
    148         }
    149         if (!isVertical(parentNode)) {
    150             String current = parentNode.getStringAttr(ANDROID_URI, ATTR_BASELINE_ALIGNED);
    151             boolean isAligned =  current == null || Boolean.valueOf(current);
    152             actions.add(RuleAction.createToggle(ACTION_BASELINE, "Toggle Baseline Alignment",
    153                     isAligned,
    154                     new PropertyCallback(Collections.singletonList(parentNode),
    155                             "Change Baseline Alignment",
    156                             ANDROID_URI, ATTR_BASELINE_ALIGNED), // TODO: Also set index?
    157                     ICON_BASELINE, 38, false));
    158         }
    159 
    160         // Gravity
    161         if (children != null && children.size() > 0) {
    162             actions.add(RuleAction.createSeparator(35));
    163 
    164             // Margins
    165             actions.add(createMarginAction(parentNode, children));
    166 
    167             // Gravity
    168             actions.add(createGravityAction(children, ATTR_LAYOUT_GRAVITY));
    169 
    170             // Weights
    171             IMenuCallback actionCallback = new IMenuCallback() {
    172                 @Override
    173                 public void action(
    174                         final @NonNull RuleAction action,
    175                         @NonNull List<? extends INode> selectedNodes,
    176                         final @Nullable String valueId,
    177                         final @Nullable Boolean newValue) {
    178                     parentNode.editXml("Change Weight", new INodeHandler() {
    179                         @Override
    180                         public void handle(@NonNull INode n) {
    181                             String id = action.getId();
    182                             if (id.equals(ACTION_WEIGHT)) {
    183                                 String weight =
    184                                     children.get(0).getStringAttr(ANDROID_URI, ATTR_LAYOUT_WEIGHT);
    185                                 if (weight == null || weight.length() == 0) {
    186                                     weight = "0.0"; //$NON-NLS-1$
    187                                 }
    188                                 weight = mRulesEngine.displayInput("Enter Weight Value:", weight,
    189                                         null);
    190                                 if (weight != null) {
    191                                     if (weight.isEmpty()) {
    192                                         weight = null; // remove attribute
    193                                     }
    194                                     for (INode child : children) {
    195                                         child.setAttribute(ANDROID_URI,
    196                                                 ATTR_LAYOUT_WEIGHT, weight);
    197                                     }
    198                                 }
    199                             } else if (id.equals(ACTION_DISTRIBUTE)) {
    200                                 distributeWeights(parentNode, parentNode.getChildren());
    201                             } else if (id.equals(ACTION_CLEAR)) {
    202                                 clearWeights(parentNode);
    203                             } else if (id.equals(ACTION_CLEAR) || id.equals(ACTION_DOMINATE)) {
    204                                 clearWeights(parentNode);
    205                                 distributeWeights(parentNode,
    206                                         children.toArray(new INode[children.size()]));
    207                             } else {
    208                                 assert id.equals(ACTION_BASELINE);
    209                             }
    210                         }
    211                     });
    212                 }
    213             };
    214             actions.add(RuleAction.createSeparator(50));
    215             actions.add(RuleAction.createAction(ACTION_DISTRIBUTE, "Distribute Weights Evenly",
    216                     actionCallback, ICON_DISTRIBUTE, 60, false /*supportsMultipleNodes*/));
    217             actions.add(RuleAction.createAction(ACTION_DOMINATE, "Assign All Weight",
    218                     actionCallback, ICON_DOMINATE, 70, false));
    219             actions.add(RuleAction.createAction(ACTION_WEIGHT, "Change Layout Weight",
    220                     actionCallback, ICON_WEIGHTS, 80, false));
    221             actions.add(RuleAction.createAction(ACTION_CLEAR, "Clear All Weights",
    222                      actionCallback, ICON_CLEAR_WEIGHTS, 90, false));
    223         }
    224     }
    225 
    226     private void distributeWeights(INode parentNode, INode[] targets) {
    227         // Any XML to get weight sum?
    228         String weightSum = parentNode.getStringAttr(ANDROID_URI,
    229                 ATTR_WEIGHT_SUM);
    230         double sum = -1.0;
    231         if (weightSum != null) {
    232             // Distribute
    233             try {
    234                 sum = Double.parseDouble(weightSum);
    235             } catch (NumberFormatException nfe) {
    236                 // Just keep using the default
    237             }
    238         }
    239         int numTargets = targets.length;
    240         double share;
    241         if (sum <= 0.0) {
    242             // The sum will be computed from the children, so just
    243             // use arbitrary amount
    244             share = 1.0;
    245         } else {
    246             share = sum / numTargets;
    247         }
    248         String value = formatFloatAttribute((float) share);
    249         String sizeAttribute = isVertical(parentNode) ?
    250                 ATTR_LAYOUT_HEIGHT : ATTR_LAYOUT_WIDTH;
    251         for (INode target : targets) {
    252             target.setAttribute(ANDROID_URI, ATTR_LAYOUT_WEIGHT, value);
    253             // Also set the width/height to 0dp to ensure actual equal
    254             // size (without this, only the remaining space is
    255             // distributed)
    256             if (VALUE_WRAP_CONTENT.equals(target.getStringAttr(ANDROID_URI, sizeAttribute))) {
    257                 target.setAttribute(ANDROID_URI, sizeAttribute, VALUE_ZERO_DP);
    258             }
    259         }
    260     }
    261 
    262     private void clearWeights(INode parentNode) {
    263         // Clear attributes
    264         String sizeAttribute = isVertical(parentNode)
    265                 ? ATTR_LAYOUT_HEIGHT : ATTR_LAYOUT_WIDTH;
    266         for (INode target : parentNode.getChildren()) {
    267             target.setAttribute(ANDROID_URI, ATTR_LAYOUT_WEIGHT, null);
    268             String size = target.getStringAttr(ANDROID_URI, sizeAttribute);
    269             if (size != null && size.startsWith("0")) { //$NON-NLS-1$
    270                 target.setAttribute(ANDROID_URI, sizeAttribute, VALUE_WRAP_CONTENT);
    271             }
    272         }
    273     }
    274 
    275     // ==== Drag'n'drop support ====
    276 
    277     @Override
    278     public DropFeedback onDropEnter(final @NonNull INode targetNode, @Nullable Object targetView,
    279             final @Nullable IDragElement[] elements) {
    280 
    281         if (elements.length == 0) {
    282             return null;
    283         }
    284 
    285         Rect bn = targetNode.getBounds();
    286         if (!bn.isValid()) {
    287             return null;
    288         }
    289 
    290         boolean isVertical = isVertical(targetNode);
    291 
    292         // Prepare a list of insertion points: X coords for horizontal, Y for
    293         // vertical.
    294         List<MatchPos> indexes = new ArrayList<MatchPos>();
    295 
    296         int last = isVertical ? bn.y : bn.x;
    297         int pos = 0;
    298         boolean lastDragged = false;
    299         int selfPos = -1;
    300         for (INode it : targetNode.getChildren()) {
    301             Rect bc = it.getBounds();
    302             if (bc.isValid()) {
    303                 // First see if this node looks like it's the same as one of the
    304                 // *dragged* bounds
    305                 boolean isDragged = false;
    306                 for (IDragElement element : elements) {
    307                     // This tries to determine if an INode corresponds to an
    308                     // IDragElement, by comparing their bounds.
    309                     if (element.isSame(it)) {
    310                         isDragged = true;
    311                         break;
    312                     }
    313                 }
    314 
    315                 // We don't want to insert drag positions before or after the
    316                 // element that is itself being dragged. However, we -do- want
    317                 // to insert a match position here, at the center, such that
    318                 // when you drag near its current position we show a match right
    319                 // where it's already positioned.
    320                 if (isDragged) {
    321                     int v = isVertical ? bc.y + (bc.h / 2) : bc.x + (bc.w / 2);
    322                     selfPos = pos;
    323                     indexes.add(new MatchPos(v, pos++));
    324                 } else if (lastDragged) {
    325                     // Even though we don't want to insert a match below, we
    326                     // need to increment the index counter such that subsequent
    327                     // lines know their correct index in the child list.
    328                     pos++;
    329                 } else {
    330                     // Add an insertion point between the last point and the
    331                     // start of this child
    332                     int v = isVertical ? bc.y : bc.x;
    333                     v = (last + v) / 2;
    334                     indexes.add(new MatchPos(v, pos++));
    335                 }
    336 
    337                 last = isVertical ? (bc.y + bc.h) : (bc.x + bc.w);
    338                 lastDragged = isDragged;
    339             } else {
    340                 // We still have to count this position even if it has no bounds, or
    341                 // subsequent children will be inserted at the wrong place
    342                 pos++;
    343             }
    344         }
    345 
    346         // Finally add an insert position after all the children - unless of
    347         // course we happened to be dragging the last element
    348         if (!lastDragged) {
    349             int v = last + 1;
    350             indexes.add(new MatchPos(v, pos));
    351         }
    352 
    353         int posCount = targetNode.getChildren().length + 1;
    354         return new DropFeedback(new LinearDropData(indexes, posCount, isVertical, selfPos),
    355                 new IFeedbackPainter() {
    356 
    357                     @Override
    358                     public void paint(@NonNull IGraphics gc, @NonNull INode node,
    359                             @NonNull DropFeedback feedback) {
    360                         // Paint callback for the LinearLayout. This is called
    361                         // by the canvas when a draw is needed.
    362                         drawFeedback(gc, node, elements, feedback);
    363                     }
    364                 });
    365     }
    366 
    367     void drawFeedback(IGraphics gc, INode node, IDragElement[] elements, DropFeedback feedback) {
    368         Rect b = node.getBounds();
    369         if (!b.isValid()) {
    370             return;
    371         }
    372 
    373         // Highlight the receiver
    374         gc.useStyle(DrawingStyle.DROP_RECIPIENT);
    375         gc.drawRect(b);
    376 
    377         gc.useStyle(DrawingStyle.DROP_ZONE);
    378 
    379         LinearDropData data = (LinearDropData) feedback.userData;
    380         boolean isVertical = data.isVertical();
    381         int selfPos = data.getSelfPos();
    382 
    383         for (MatchPos it : data.getIndexes()) {
    384             int i = it.getDistance();
    385             int pos = it.getPosition();
    386             // Don't show insert drop zones for "self"-index since that one goes
    387             // right through the center of the widget rather than in a sibling
    388             // position
    389             if (pos != selfPos) {
    390                 if (isVertical) {
    391                     // draw horizontal lines
    392                     gc.drawLine(b.x, i, b.x + b.w, i);
    393                 } else {
    394                     // draw vertical lines
    395                     gc.drawLine(i, b.y, i, b.y + b.h);
    396                 }
    397             }
    398         }
    399 
    400         Integer currX = data.getCurrX();
    401         Integer currY = data.getCurrY();
    402 
    403         if (currX != null && currY != null) {
    404             gc.useStyle(DrawingStyle.DROP_ZONE_ACTIVE);
    405 
    406             int x = currX;
    407             int y = currY;
    408 
    409             Rect be = elements[0].getBounds();
    410 
    411             // Draw a clear line at the closest drop zone (unless we're over the
    412             // dragged element itself)
    413             if (data.getInsertPos() != selfPos || selfPos == -1) {
    414                 gc.useStyle(DrawingStyle.DROP_PREVIEW);
    415                 if (data.getWidth() != null) {
    416                     int width = data.getWidth();
    417                     int fromX = x - width / 2;
    418                     int toX = x + width / 2;
    419                     gc.drawLine(fromX, y, toX, y);
    420                 } else if (data.getHeight() != null) {
    421                     int height = data.getHeight();
    422                     int fromY = y - height / 2;
    423                     int toY = y + height / 2;
    424                     gc.drawLine(x, fromY, x, toY);
    425                 }
    426             }
    427 
    428             if (be.isValid()) {
    429                 boolean isLast = data.isLastPosition();
    430 
    431                 // At least the first element has a bound. Draw rectangles for
    432                 // all dropped elements with valid bounds, offset at the drop
    433                 // point.
    434                 int offsetX;
    435                 int offsetY;
    436                 if (isVertical) {
    437                     offsetX = b.x - be.x;
    438                     offsetY = currY - be.y - (isLast ? 0 : (be.h / 2));
    439 
    440                 } else {
    441                     offsetX = currX - be.x - (isLast ? 0 : (be.w / 2));
    442                     offsetY = b.y - be.y;
    443                 }
    444 
    445                 gc.useStyle(DrawingStyle.DROP_PREVIEW);
    446                 for (IDragElement element : elements) {
    447                     Rect bounds = element.getBounds();
    448                     if (bounds.isValid() && (bounds.w > b.w || bounds.h > b.h) &&
    449                             node.getChildren().length == 0) {
    450                         // The bounds of the child does not fully fit inside the target.
    451                         // Limit the bounds to the layout bounds (but only when there
    452                         // are no children, since otherwise positioning around the existing
    453                         // children gets difficult)
    454                         final int px, py, pw, ph;
    455                         if (bounds.w > b.w) {
    456                             px = b.x;
    457                             pw = b.w;
    458                         } else {
    459                             px = bounds.x + offsetX;
    460                             pw = bounds.w;
    461                         }
    462                         if (bounds.h > b.h) {
    463                             py = b.y;
    464                             ph = b.h;
    465                         } else {
    466                             py = bounds.y + offsetY;
    467                             ph = bounds.h;
    468                         }
    469                         Rect within = new Rect(px, py, pw, ph);
    470                         gc.drawRect(within);
    471                     } else {
    472                         drawElement(gc, element, offsetX, offsetY);
    473                     }
    474                 }
    475             }
    476         }
    477     }
    478 
    479     @Override
    480     public DropFeedback onDropMove(@NonNull INode targetNode, @NonNull IDragElement[] elements,
    481             @Nullable DropFeedback feedback, @NonNull Point p) {
    482         Rect b = targetNode.getBounds();
    483         if (!b.isValid()) {
    484             return feedback;
    485         }
    486 
    487         LinearDropData data = (LinearDropData) feedback.userData;
    488         boolean isVertical = data.isVertical();
    489 
    490         int bestDist = Integer.MAX_VALUE;
    491         int bestIndex = Integer.MIN_VALUE;
    492         Integer bestPos = null;
    493 
    494         for (MatchPos index : data.getIndexes()) {
    495             int i = index.getDistance();
    496             int pos = index.getPosition();
    497             int dist = (isVertical ? p.y : p.x) - i;
    498             if (dist < 0)
    499                 dist = -dist;
    500             if (dist < bestDist) {
    501                 bestDist = dist;
    502                 bestIndex = i;
    503                 bestPos = pos;
    504                 if (bestDist <= 0)
    505                     break;
    506             }
    507         }
    508 
    509         if (bestIndex != Integer.MIN_VALUE) {
    510             Integer oldX = data.getCurrX();
    511             Integer oldY = data.getCurrY();
    512 
    513             if (isVertical) {
    514                 data.setCurrX(b.x + b.w / 2);
    515                 data.setCurrY(bestIndex);
    516                 data.setWidth(b.w);
    517                 data.setHeight(null);
    518             } else {
    519                 data.setCurrX(bestIndex);
    520                 data.setCurrY(b.y + b.h / 2);
    521                 data.setWidth(null);
    522                 data.setHeight(b.h);
    523             }
    524 
    525             data.setInsertPos(bestPos);
    526 
    527             feedback.requestPaint = !equals(oldX, data.getCurrX())
    528                     || !equals(oldY, data.getCurrY());
    529         }
    530 
    531         return feedback;
    532     }
    533 
    534     private static boolean equals(Integer i1, Integer i2) {
    535         if (i1 == i2) {
    536             return true;
    537         } else if (i1 != null) {
    538             return i1.equals(i2);
    539         } else {
    540             // We know i2 != null
    541             return i2.equals(i1);
    542         }
    543     }
    544 
    545     @Override
    546     public void onDropLeave(@NonNull INode targetNode, @NonNull IDragElement[] elements,
    547             @Nullable DropFeedback feedback) {
    548         // ignore
    549     }
    550 
    551     @Override
    552     public void onDropped(final @NonNull INode targetNode, final @NonNull IDragElement[] elements,
    553             final @Nullable DropFeedback feedback, final @NonNull Point p) {
    554 
    555         LinearDropData data = (LinearDropData) feedback.userData;
    556         final int initialInsertPos = data.getInsertPos();
    557         insertAt(targetNode, elements, feedback.isCopy || !feedback.sameCanvas, initialInsertPos);
    558     }
    559 
    560     @Override
    561     public void onChildInserted(@NonNull INode node, @NonNull INode parent,
    562             @NonNull InsertType insertType) {
    563         if (insertType == InsertType.MOVE_WITHIN) {
    564             // Don't adjust widths/heights/weights when just moving within a single
    565             // LinearLayout
    566             return;
    567         }
    568 
    569         // Attempt to set fill-properties on newly added views such that for example,
    570         // in a vertical layout, a text field defaults to filling horizontally, but not
    571         // vertically.
    572         String fqcn = node.getFqcn();
    573         IViewMetadata metadata = mRulesEngine.getMetadata(fqcn);
    574         if (metadata != null) {
    575             boolean vertical = isVertical(parent);
    576             FillPreference fill = metadata.getFillPreference();
    577             String fillParent = getFillParentValueName();
    578             if (fill.fillHorizontally(vertical)) {
    579                 node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, fillParent);
    580             } else if (!vertical && fill == FillPreference.WIDTH_IN_VERTICAL) {
    581                 // In a horizontal layout, make views that would fill horizontally in a
    582                 // vertical layout have a non-zero weight instead. This will make the item
    583                 // fill but only enough to allow other views to be shown as well.
    584                 // (However, for drags within the same layout we do not touch
    585                 // the weight, since it might already have been tweaked to a particular
    586                 // value)
    587                 node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WEIGHT, VALUE_1);
    588             }
    589             if (fill.fillVertically(vertical)) {
    590                 node.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT, fillParent);
    591             }
    592         }
    593 
    594         // If you insert into a layout that already is using layout weights,
    595         // and all the layout weights are the same (nonzero) value, then use
    596         // the same weight for this new layout as well. Also duplicate the 0dip/0px/0dp
    597         // sizes, if used.
    598         boolean duplicateWeight = true;
    599         boolean duplicate0dip = true;
    600         String sameWeight = null;
    601         String sizeAttribute = isVertical(parent) ? ATTR_LAYOUT_HEIGHT : ATTR_LAYOUT_WIDTH;
    602         for (INode target : parent.getChildren()) {
    603             if (target == node) {
    604                 continue;
    605             }
    606             String weight = target.getStringAttr(ANDROID_URI, ATTR_LAYOUT_WEIGHT);
    607             if (weight == null || weight.length() == 0) {
    608                 duplicateWeight = false;
    609                 break;
    610             } else if (sameWeight != null && !sameWeight.equals(weight)) {
    611                 duplicateWeight = false;
    612             } else {
    613                 sameWeight = weight;
    614             }
    615             String size = target.getStringAttr(ANDROID_URI, sizeAttribute);
    616             if (size != null && !size.startsWith("0")) { //$NON-NLS-1$
    617                 duplicate0dip = false;
    618                 break;
    619             }
    620         }
    621         if (duplicateWeight && sameWeight != null) {
    622             node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WEIGHT, sameWeight);
    623             if (duplicate0dip) {
    624                 node.setAttribute(ANDROID_URI, sizeAttribute, VALUE_ZERO_DP);
    625             }
    626         }
    627     }
    628 
    629     /** A possible match position */
    630     private static class MatchPos {
    631         /** The pixel distance */
    632         private int mDistance;
    633         /** The position among siblings */
    634         private int mPosition;
    635 
    636         public MatchPos(int distance, int position) {
    637             mDistance = distance;
    638             mPosition = position;
    639         }
    640 
    641         @Override
    642         public String toString() {
    643             return "MatchPos [distance=" + mDistance //$NON-NLS-1$
    644                     + ", position=" + mPosition      //$NON-NLS-1$
    645                     + "]";                           //$NON-NLS-1$
    646         }
    647 
    648         private int getDistance() {
    649             return mDistance;
    650         }
    651 
    652         private int getPosition() {
    653             return mPosition;
    654         }
    655     }
    656 
    657     private static class LinearDropData {
    658         /** Vertical layout? */
    659         private final boolean mVertical;
    660 
    661         /** Insert points (pixels + index) */
    662         private final List<MatchPos> mIndexes;
    663 
    664         /** Number of insert positions in the target node */
    665         private final int mNumPositions;
    666 
    667         /** Current marker X position */
    668         private Integer mCurrX;
    669 
    670         /** Current marker Y position */
    671         private Integer mCurrY;
    672 
    673         /** Position of the dragged element in this layout (or
    674             -1 if the dragged element is from elsewhere) */
    675         private final int mSelfPos;
    676 
    677         /** Current drop insert index (-1 for "at the end") */
    678         private int mInsertPos = -1;
    679 
    680         /** width of match line if it's a horizontal one */
    681         private Integer mWidth;
    682 
    683         /** height of match line if it's a vertical one */
    684         private Integer mHeight;
    685 
    686         public LinearDropData(List<MatchPos> indexes, int numPositions,
    687                 boolean isVertical, int selfPos) {
    688             mIndexes = indexes;
    689             mNumPositions = numPositions;
    690             mVertical = isVertical;
    691             mSelfPos = selfPos;
    692         }
    693 
    694         @Override
    695         public String toString() {
    696             return "LinearDropData [currX=" + mCurrX //$NON-NLS-1$
    697                     + ", currY=" + mCurrY //$NON-NLS-1$
    698                     + ", height=" + mHeight //$NON-NLS-1$
    699                     + ", indexes=" + mIndexes //$NON-NLS-1$
    700                     + ", insertPos=" + mInsertPos //$NON-NLS-1$
    701                     + ", isVertical=" + mVertical //$NON-NLS-1$
    702                     + ", selfPos=" + mSelfPos //$NON-NLS-1$
    703                     + ", width=" + mWidth //$NON-NLS-1$
    704                     + "]"; //$NON-NLS-1$
    705         }
    706 
    707         private boolean isVertical() {
    708             return mVertical;
    709         }
    710 
    711         private void setCurrX(Integer currX) {
    712             mCurrX = currX;
    713         }
    714 
    715         private Integer getCurrX() {
    716             return mCurrX;
    717         }
    718 
    719         private void setCurrY(Integer currY) {
    720             mCurrY = currY;
    721         }
    722 
    723         private Integer getCurrY() {
    724             return mCurrY;
    725         }
    726 
    727         private int getSelfPos() {
    728             return mSelfPos;
    729         }
    730 
    731         private void setInsertPos(int insertPos) {
    732             mInsertPos = insertPos;
    733         }
    734 
    735         private int getInsertPos() {
    736             return mInsertPos;
    737         }
    738 
    739         private List<MatchPos> getIndexes() {
    740             return mIndexes;
    741         }
    742 
    743         private void setWidth(Integer width) {
    744             mWidth = width;
    745         }
    746 
    747         private Integer getWidth() {
    748             return mWidth;
    749         }
    750 
    751         private void setHeight(Integer height) {
    752             mHeight = height;
    753         }
    754 
    755         private Integer getHeight() {
    756             return mHeight;
    757         }
    758 
    759         /**
    760          * Returns true if we are inserting into the last position
    761          *
    762          * @return true if we are inserting into the last position
    763          */
    764         public boolean isLastPosition() {
    765             return mInsertPos == mNumPositions - 1;
    766         }
    767     }
    768 
    769     /** Custom resize state used during linear layout resizing */
    770     private class LinearResizeState extends ResizeState {
    771         /** Whether the node should be assigned a new weight */
    772         public boolean useWeight;
    773         /** Weight sum to be applied to the parent */
    774         private float mNewWeightSum;
    775         /** The weight to be set on the node (provided {@link #useWeight} is true) */
    776         private float mWeight;
    777         /** Map from nodes to preferred bounds of nodes where the weights have been cleared */
    778         public final Map<INode, Rect> unweightedSizes;
    779         /** Total required size required by the siblings <b>without</b> weights */
    780         public int totalLength;
    781         /** List of nodes which should have their weights cleared */
    782         public List<INode> mClearWeights;
    783 
    784         private LinearResizeState(BaseLayoutRule rule, INode layout, Object layoutView,
    785                 INode node) {
    786             super(rule, layout, layoutView, node);
    787 
    788             unweightedSizes = mRulesEngine.measureChildren(layout,
    789                     new IClientRulesEngine.AttributeFilter() {
    790                         @Override
    791                         public String getAttribute(@NonNull INode n, @Nullable String namespace,
    792                                 @NonNull String localName) {
    793                             // Clear out layout weights; we need to measure the unweighted sizes
    794                             // of the children
    795                             if (ATTR_LAYOUT_WEIGHT.equals(localName)
    796                                     && SdkConstants.NS_RESOURCES.equals(namespace)) {
    797                                 return ""; //$NON-NLS-1$
    798                             }
    799 
    800                             return null;
    801                         }
    802                     });
    803 
    804             // Compute total required size required by the siblings *without* weights
    805             totalLength = 0;
    806             final boolean isVertical = isVertical(layout);
    807             for (Map.Entry<INode, Rect> entry : unweightedSizes.entrySet()) {
    808                 Rect preferredSize = entry.getValue();
    809                 if (isVertical) {
    810                     totalLength += preferredSize.h;
    811                 } else {
    812                     totalLength += preferredSize.w;
    813                 }
    814             }
    815         }
    816 
    817         /** Resets the computed state */
    818         void reset() {
    819             mNewWeightSum = -1;
    820             useWeight = false;
    821             mClearWeights = null;
    822         }
    823 
    824         /** Sets a weight to be applied to the node */
    825         void setWeight(float weight) {
    826             useWeight = true;
    827             mWeight = weight;
    828         }
    829 
    830         /** Sets a weight sum to be applied to the parent layout */
    831         void setWeightSum(float weightSum) {
    832             mNewWeightSum = weightSum;
    833         }
    834 
    835         /** Marks that the given node should be cleared when applying the new size */
    836         void clearWeight(INode n) {
    837             if (mClearWeights == null) {
    838                 mClearWeights = new ArrayList<INode>();
    839             }
    840             mClearWeights.add(n);
    841         }
    842 
    843         /** Applies the state to the nodes */
    844         public void apply() {
    845             assert useWeight;
    846 
    847             String value = mWeight > 0 ? formatFloatAttribute(mWeight) : null;
    848             node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WEIGHT, value);
    849 
    850             if (mClearWeights != null) {
    851                 for (INode n : mClearWeights) {
    852                     if (getWeight(n) > 0.0f) {
    853                         n.setAttribute(ANDROID_URI, ATTR_LAYOUT_WEIGHT, null);
    854                     }
    855                 }
    856             }
    857 
    858             if (mNewWeightSum > 0.0) {
    859                 layout.setAttribute(ANDROID_URI, ATTR_WEIGHT_SUM,
    860                         formatFloatAttribute(mNewWeightSum));
    861             }
    862         }
    863     }
    864 
    865     @Override
    866     protected ResizeState createResizeState(INode layout, Object layoutView, INode node) {
    867         return new LinearResizeState(this, layout, layoutView, node);
    868     }
    869 
    870     protected void updateResizeState(LinearResizeState resizeState, final INode node, INode layout,
    871             Rect oldBounds, Rect newBounds, SegmentType horizontalEdge,
    872             SegmentType verticalEdge) {
    873         // Update the resize state.
    874         // This method attempts to compute a new layout weight to be used in the direction
    875         // of the linear layout. If the superclass has already determined that we can snap to
    876         // a wrap_content or match_parent boundary, we prefer that. Otherwise, we attempt to
    877         // compute a layout weight - which can fail if the size is too big (not enough room),
    878         // or if the size is too small (smaller than the natural width of the node), and so on.
    879         // In that case this method just aborts, which will leave the resize state object
    880         // in such a state that it will call the superclass to resize instead, which will fall
    881         // back to device independent pixel sizing.
    882         resizeState.reset();
    883 
    884         if (oldBounds.equals(newBounds)) {
    885             return;
    886         }
    887 
    888         // If we're setting the width/height to wrap_content/match_parent in the dimension of the
    889         // linear layout, then just apply wrap_content and clear weights.
    890         boolean isVertical = isVertical(layout);
    891         if (!isVertical && verticalEdge != null) {
    892             if (resizeState.wrapWidth || resizeState.fillWidth) {
    893                 resizeState.clearWeight(node);
    894                 return;
    895             }
    896             if (newBounds.w == oldBounds.w) {
    897                 return;
    898             }
    899         }
    900 
    901         if (isVertical && horizontalEdge != null) {
    902             if (resizeState.wrapHeight || resizeState.fillHeight) {
    903                 resizeState.clearWeight(node);
    904                 return;
    905             }
    906             if (newBounds.h == oldBounds.h) {
    907                 return;
    908             }
    909         }
    910 
    911         // Compute weight sum
    912         float sum = getWeightSum(layout);
    913         if (sum <= 0.0f) {
    914             sum = 1.0f;
    915             resizeState.setWeightSum(sum);
    916         }
    917 
    918         // If the new size of the node is smaller than its preferred/wrap_content size,
    919         // then we cannot use weights to size it; switch to pixel-based sizing instead
    920         Map<INode, Rect> sizes = resizeState.unweightedSizes;
    921         Rect nodePreferredSize = sizes.get(node);
    922         if (nodePreferredSize != null) {
    923             if (horizontalEdge != null && newBounds.h < nodePreferredSize.h ||
    924                     verticalEdge != null && newBounds.w < nodePreferredSize.w) {
    925                 return;
    926             }
    927         }
    928 
    929         Rect layoutBounds = layout.getBounds();
    930         int remaining = (isVertical ? layoutBounds.h : layoutBounds.w) - resizeState.totalLength;
    931         Rect nodeBounds = sizes.get(node);
    932         if (nodeBounds == null) {
    933             return;
    934         }
    935 
    936         if (remaining > 0) {
    937             int missing = 0;
    938             if (isVertical) {
    939                 if (newBounds.h > nodeBounds.h) {
    940                     missing = newBounds.h - nodeBounds.h;
    941                 } else if (newBounds.h > resizeState.wrapBounds.h) {
    942                     // The weights concern how much space to ADD to the view.
    943                     // What if we have resized it to a size *smaller* than its current
    944                     // size without the weight delta? This can happen if you for example
    945                     // have set a hardcoded size, such as 500dp, and then size it to some
    946                     // smaller size.
    947                     missing = newBounds.h - resizeState.wrapBounds.h;
    948                     remaining += nodeBounds.h - resizeState.wrapBounds.h;
    949                     resizeState.wrapHeight = true;
    950                 }
    951             } else {
    952                 if (newBounds.w > nodeBounds.w) {
    953                     missing = newBounds.w - nodeBounds.w;
    954                 } else if (newBounds.w > resizeState.wrapBounds.w) {
    955                     missing = newBounds.w - resizeState.wrapBounds.w;
    956                     remaining += nodeBounds.w - resizeState.wrapBounds.w;
    957                     resizeState.wrapWidth = true;
    958                 }
    959             }
    960             if (missing > 0) {
    961                 // (weight / weightSum) * remaining = missing, so
    962                 // weight = missing * weightSum / remaining
    963                 float weight = missing * sum / remaining;
    964                 resizeState.setWeight(weight);
    965             }
    966         }
    967     }
    968 
    969     /**
    970      * {@inheritDoc}
    971      * <p>
    972      * Overridden in this layout in order to make resizing affect the layout_weight
    973      * attribute instead of the layout_width (for horizontal LinearLayouts) or
    974      * layout_height (for vertical LinearLayouts).
    975      */
    976     @Override
    977     protected void setNewSizeBounds(ResizeState state, final INode node, INode layout,
    978             Rect oldBounds, Rect newBounds, SegmentType horizontalEdge,
    979             SegmentType verticalEdge) {
    980         LinearResizeState resizeState = (LinearResizeState) state;
    981         updateResizeState(resizeState, node, layout, oldBounds, newBounds,
    982                 horizontalEdge, verticalEdge);
    983 
    984         if (resizeState.useWeight) {
    985             resizeState.apply();
    986 
    987             // Handle resizing in the opposite dimension of the layout
    988             final boolean isVertical = isVertical(layout);
    989             if (!isVertical && horizontalEdge != null) {
    990                 if (newBounds.h != oldBounds.h || resizeState.wrapHeight
    991                         || resizeState.fillHeight) {
    992                     node.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT,
    993                             resizeState.getHeightAttribute());
    994                 }
    995             }
    996             if (isVertical && verticalEdge != null) {
    997                 if (newBounds.w != oldBounds.w || resizeState.wrapWidth || resizeState.fillWidth) {
    998                     node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH,
    999                             resizeState.getWidthAttribute());
   1000                 }
   1001             }
   1002         } else {
   1003             node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WEIGHT, null);
   1004             super.setNewSizeBounds(resizeState, node, layout, oldBounds, newBounds,
   1005                     horizontalEdge, verticalEdge);
   1006         }
   1007     }
   1008 
   1009     @Override
   1010     protected String getResizeUpdateMessage(ResizeState state, INode child, INode parent,
   1011             Rect newBounds, SegmentType horizontalEdge, SegmentType verticalEdge) {
   1012         LinearResizeState resizeState = (LinearResizeState) state;
   1013         updateResizeState(resizeState, child, parent, child.getBounds(), newBounds,
   1014                 horizontalEdge, verticalEdge);
   1015 
   1016         if (resizeState.useWeight) {
   1017             String weight = formatFloatAttribute(resizeState.mWeight);
   1018             String dimension = String.format("weight %1$s", weight);
   1019 
   1020             String width;
   1021             String height;
   1022             if (isVertical(parent)) {
   1023                 width = resizeState.getWidthAttribute();
   1024                 height = dimension;
   1025             } else {
   1026                 width = dimension;
   1027                 height = resizeState.getHeightAttribute();
   1028             }
   1029 
   1030             if (horizontalEdge == null) {
   1031                 return width;
   1032             } else if (verticalEdge == null) {
   1033                 return height;
   1034             } else {
   1035                 // U+00D7: Unicode for multiplication sign
   1036                 return String.format("%s \u00D7 %s", width, height);
   1037             }
   1038         } else {
   1039             return super.getResizeUpdateMessage(state, child, parent, newBounds,
   1040                     horizontalEdge, verticalEdge);
   1041         }
   1042     }
   1043 
   1044     /**
   1045      * Returns the layout weight of of the given child of a LinearLayout, or 0.0 if it
   1046      * does not define a weight
   1047      */
   1048     private static float getWeight(INode linearLayoutChild) {
   1049         String weight = linearLayoutChild.getStringAttr(ANDROID_URI, ATTR_LAYOUT_WEIGHT);
   1050         if (weight != null && weight.length() > 0) {
   1051             try {
   1052                 return Float.parseFloat(weight);
   1053             } catch (NumberFormatException nfe) {
   1054                 AdtPlugin.log(nfe, "Invalid weight %1$s", weight);
   1055             }
   1056         }
   1057 
   1058         return 0.0f;
   1059     }
   1060 
   1061     /**
   1062      * Returns the sum of all the layout weights of the children in the given LinearLayout
   1063      *
   1064      * @param linearLayout the layout to compute the total sum for
   1065      * @return the total sum of all the layout weights in the given layout
   1066      */
   1067     private static float getWeightSum(INode linearLayout) {
   1068         String weightSum = linearLayout.getStringAttr(ANDROID_URI,
   1069                 ATTR_WEIGHT_SUM);
   1070         float sum = -1.0f;
   1071         if (weightSum != null) {
   1072             // Distribute
   1073             try {
   1074                 sum = Float.parseFloat(weightSum);
   1075                 return sum;
   1076             } catch (NumberFormatException nfe) {
   1077                 // Just keep using the default
   1078             }
   1079         }
   1080 
   1081         return getSumOfWeights(linearLayout);
   1082     }
   1083 
   1084     private static float getSumOfWeights(INode linearLayout) {
   1085         float sum = 0.0f;
   1086         for (INode child : linearLayout.getChildren()) {
   1087             sum += getWeight(child);
   1088         }
   1089 
   1090         return sum;
   1091     }
   1092 }
   1093