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