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