• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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      */
getCurrentOrientation(final INode node)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      */
isVertical(INode node)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      */
supportsOrientation()121     protected boolean supportsOrientation() {
122         return true;
123     }
124 
125     @Override
addLayoutActions(List<RuleAction> actions, final INode parentNode, final List<? extends INode> children)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 
distributeWeights(INode parentNode, INode[] targets)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 
clearWeights(INode parentNode)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
onDropEnter(final INode targetNode, Object targetView, final IDragElement[] elements)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