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