| /* |
| * Copyright (C) 2011 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.tools.lint.checks; |
| |
| import static com.android.SdkConstants.ANDROID_URI; |
| import static com.android.SdkConstants.ATTR_BASELINE_ALIGNED; |
| import static com.android.SdkConstants.ATTR_ID; |
| import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT; |
| import static com.android.SdkConstants.ATTR_LAYOUT_WEIGHT; |
| import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH; |
| import static com.android.SdkConstants.ATTR_ORIENTATION; |
| import static com.android.SdkConstants.ATTR_STYLE; |
| import static com.android.SdkConstants.LINEAR_LAYOUT; |
| import static com.android.SdkConstants.RADIO_GROUP; |
| import static com.android.SdkConstants.VALUE_FILL_PARENT; |
| import static com.android.SdkConstants.VALUE_MATCH_PARENT; |
| import static com.android.SdkConstants.VALUE_VERTICAL; |
| import static com.android.SdkConstants.VIEW; |
| import static com.android.SdkConstants.VIEW_FRAGMENT; |
| import static com.android.SdkConstants.VIEW_INCLUDE; |
| import static com.android.SdkConstants.VIEW_TAG; |
| |
| import com.android.annotations.NonNull; |
| import com.android.ide.common.rendering.api.ResourceValue; |
| import com.android.tools.lint.client.api.SdkInfo; |
| import com.android.tools.lint.detector.api.Category; |
| import com.android.tools.lint.detector.api.Implementation; |
| import com.android.tools.lint.detector.api.Issue; |
| import com.android.tools.lint.detector.api.LayoutDetector; |
| import com.android.tools.lint.detector.api.LintUtils; |
| import com.android.tools.lint.detector.api.Scope; |
| import com.android.tools.lint.detector.api.Severity; |
| import com.android.tools.lint.detector.api.Speed; |
| import com.android.tools.lint.detector.api.XmlContext; |
| |
| import org.w3c.dom.Attr; |
| import org.w3c.dom.Element; |
| import org.w3c.dom.Node; |
| |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.IdentityHashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| |
| /** |
| * Checks whether a layout_weight is declared inefficiently. |
| */ |
| public class InefficientWeightDetector extends LayoutDetector { |
| |
| private static final Implementation IMPLEMENTATION = new Implementation( |
| InefficientWeightDetector.class, |
| Scope.RESOURCE_FILE_SCOPE); |
| |
| /** Can a weight be replaced with 0dp instead for better performance? */ |
| public static final Issue INEFFICIENT_WEIGHT = Issue.create( |
| "InefficientWeight", //$NON-NLS-1$ |
| "Inefficient layout weight", |
| "When only a single widget in a LinearLayout defines a weight, it is more " + |
| "efficient to assign a width/height of `0dp` to it since it will absorb all " + |
| "the remaining space anyway. With a declared width/height of `0dp` it " + |
| "does not have to measure its own size first.", |
| Category.PERFORMANCE, |
| 3, |
| Severity.WARNING, |
| IMPLEMENTATION); |
| |
| /** Are weights nested? */ |
| public static final Issue NESTED_WEIGHTS = Issue.create( |
| "NestedWeights", //$NON-NLS-1$ |
| "Nested layout weights", |
| "Layout weights require a widget to be measured twice. When a LinearLayout with " + |
| "non-zero weights is nested inside another LinearLayout with non-zero weights, " + |
| "then the number of measurements increase exponentially.", |
| Category.PERFORMANCE, |
| 3, |
| Severity.WARNING, |
| IMPLEMENTATION); |
| |
| /** Should a LinearLayout set android:baselineAligned? */ |
| public static final Issue BASELINE_WEIGHTS = Issue.create( |
| "DisableBaselineAlignment", //$NON-NLS-1$ |
| "Missing `baselineAligned` attribute", |
| "When a LinearLayout is used to distribute the space proportionally between " + |
| "nested layouts, the baseline alignment property should be turned off to " + |
| "make the layout computation faster.", |
| Category.PERFORMANCE, |
| 3, |
| Severity.WARNING, |
| IMPLEMENTATION); |
| |
| /** Using 0dp on the wrong dimension */ |
| public static final Issue WRONG_0DP = Issue.create( |
| "Suspicious0dp", //$NON-NLS-1$ |
| "Suspicious 0dp dimension", |
| |
| "Using 0dp as the width in a horizontal LinearLayout with weights is a useful " + |
| "trick to ensure that only the weights (and not the intrinsic sizes) are used " + |
| "when sizing the children.\n" + |
| "\n" + |
| "However, if you use 0dp for the opposite dimension, the view will be invisible. " + |
| "This can happen if you change the orientation of a layout without also flipping " + |
| "the 0dp dimension in all the children.", |
| Category.CORRECTNESS, |
| 6, |
| Severity.ERROR, |
| IMPLEMENTATION); |
| |
| /** Missing explicit orientation */ |
| public static final Issue ORIENTATION = Issue.create( |
| "Orientation", //$NON-NLS-1$ |
| "Missing explicit orientation", |
| |
| "The default orientation of a LinearLayout is horizontal. It's pretty easy to " |
| + "believe that the layout is vertical, add multiple children to it, and wonder " |
| + "why only the first child is visible (when the subsequent children are " |
| + "off screen to the right). This lint rule helps pinpoint this issue by " |
| + "warning whenever a LinearLayout is used with an implicit orientation " |
| + "and multiple children.\n" |
| + "\n" |
| + "It also checks for empty LinearLayouts without an `orientation` attribute " |
| + "that also defines an `id` attribute. This catches the scenarios where " |
| + "children will be added to the `LinearLayout` dynamically. ", |
| |
| Category.CORRECTNESS, |
| 2, |
| Severity.ERROR, |
| IMPLEMENTATION); |
| |
| /** |
| * Map from element to whether that element has a non-zero linear layout |
| * weight or has an ancestor which does |
| */ |
| private final Map<Node, Boolean> mInsideWeight = new IdentityHashMap<Node, Boolean>(); |
| |
| /** Constructs a new {@link InefficientWeightDetector} */ |
| public InefficientWeightDetector() { |
| } |
| |
| @NonNull |
| @Override |
| public Speed getSpeed() { |
| return Speed.FAST; |
| } |
| |
| @Override |
| public Collection<String> getApplicableElements() { |
| return Collections.singletonList(LINEAR_LAYOUT); |
| } |
| |
| @Override |
| public void visitElement(@NonNull XmlContext context, @NonNull Element element) { |
| List<Element> children = LintUtils.getChildren(element); |
| // See if there is exactly one child with a weight |
| boolean multipleWeights = false; |
| Element weightChild = null; |
| boolean checkNesting = context.isEnabled(NESTED_WEIGHTS); |
| for (Element child : children) { |
| if (child.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_WEIGHT)) { |
| if (weightChild != null) { |
| // More than one child defining a weight! |
| multipleWeights = true; |
| } else //noinspection ConstantConditions |
| if (!multipleWeights) { |
| weightChild = child; |
| } |
| |
| if (checkNesting) { |
| mInsideWeight.put(child, Boolean.TRUE); |
| |
| Boolean inside = mInsideWeight.get(element); |
| if (inside == null) { |
| mInsideWeight.put(element, Boolean.FALSE); |
| } else if (inside) { |
| Attr sizeNode = child.getAttributeNodeNS(ANDROID_URI, ATTR_LAYOUT_WEIGHT); |
| context.report(NESTED_WEIGHTS, sizeNode, |
| context.getLocation(sizeNode), |
| "Nested weights are bad for performance"); |
| // Don't warn again |
| checkNesting = false; |
| } |
| } |
| } |
| } |
| |
| String orientation = element.getAttributeNS(ANDROID_URI, ATTR_ORIENTATION); |
| if (children.size() >= 2 && (orientation == null || orientation.isEmpty()) |
| && context.isEnabled(ORIENTATION)) { |
| // See if at least one of the children, except the last one, sets layout_width |
| // to match_parent (or fill_parent), in an implicitly horizontal layout, since |
| // that might mean the last child won't be visible. This is a source of confusion |
| // for new Android developers. |
| boolean maxWidthSet = false; |
| Iterator<Element> iterator = children.iterator(); |
| while (iterator.hasNext()) { |
| Element child = iterator.next(); |
| if (!iterator.hasNext()) { // Don't check the last one |
| break; |
| } |
| String width = child.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH); |
| if (VALUE_MATCH_PARENT.equals(width) || VALUE_FILL_PARENT.equals(width)) { |
| // Also check that weights are not set here; this affects the computation |
| // a bit and the child may not fill up the whole linear layout |
| if (!child.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_WEIGHT)) { |
| maxWidthSet = true; |
| break; |
| } |
| } |
| } |
| if (maxWidthSet && !element.hasAttribute(ATTR_STYLE)) { |
| String message = "Wrong orientation? No orientation specified, and the default " |
| + "is horizontal, yet this layout has multiple children where at " |
| + "least one has `layout_width=\"match_parent\"`"; |
| context.report(ORIENTATION, element, context.getLocation(element), message); |
| } |
| } else if (children.isEmpty() && (orientation == null || orientation.isEmpty()) |
| && context.isEnabled(ORIENTATION) |
| && element.hasAttributeNS(ANDROID_URI, ATTR_ID)) { |
| boolean ignore; |
| if (element.hasAttribute(ATTR_STYLE)) { |
| if (context.getClient().supportsProjectResources()) { |
| List<ResourceValue> values = LintUtils.getStyleAttributes( |
| context.getMainProject(), context.getClient(), |
| element.getAttribute(ATTR_STYLE), ANDROID_URI, ATTR_ORIENTATION); |
| ignore = values != null && !values.isEmpty(); |
| } else { |
| ignore = true; |
| } |
| } else { |
| ignore = false; |
| } |
| if (!ignore) { |
| String message = "No orientation specified, and the default is horizontal. " |
| + "This is a common source of bugs when children are added dynamically."; |
| context.report(ORIENTATION, element, context.getLocation(element), message); |
| } |
| } |
| |
| if (context.isEnabled(BASELINE_WEIGHTS) && weightChild != null |
| && !VALUE_VERTICAL.equals(orientation) |
| && !element.hasAttributeNS(ANDROID_URI, ATTR_BASELINE_ALIGNED)) { |
| // See if all the children are layouts |
| boolean allChildrenAreLayouts = !children.isEmpty(); |
| SdkInfo sdkInfo = context.getClient().getSdkInfo(context.getProject()); |
| for (Element child : children) { |
| String tagName = child.getTagName(); |
| if (!(sdkInfo.isLayout(tagName) |
| // RadioGroup is a layout, but one which possibly should be base aligned |
| && !tagName.equals(RADIO_GROUP) |
| // Consider <fragment> tags as layouts for the purposes of this check |
| || VIEW_FRAGMENT.equals(tagName) |
| // Ditto for <include> tags |
| || VIEW_INCLUDE.equals(tagName))) { |
| allChildrenAreLayouts = false; |
| } |
| } |
| if (allChildrenAreLayouts) { |
| context.report(BASELINE_WEIGHTS, |
| element, |
| context.getLocation(element), |
| "Set `android:baselineAligned=\"false\"` on this element for better performance"); |
| } |
| } |
| |
| if (context.isEnabled(INEFFICIENT_WEIGHT) |
| && weightChild != null && !multipleWeights) { |
| String dimension; |
| if (VALUE_VERTICAL.equals(orientation)) { |
| dimension = ATTR_LAYOUT_HEIGHT; |
| } else { |
| dimension = ATTR_LAYOUT_WIDTH; |
| } |
| Attr sizeNode = weightChild.getAttributeNodeNS(ANDROID_URI, dimension); |
| String size = sizeNode != null ? sizeNode.getValue() : "(undefined)"; |
| if (sizeNode == null && weightChild.hasAttribute(ATTR_STYLE)) { |
| String style = weightChild.getAttribute(ATTR_STYLE); |
| List<ResourceValue> sizes = LintUtils.getStyleAttributes(context.getMainProject(), |
| context.getClient(), style, ANDROID_URI, dimension); |
| if (sizes != null) { |
| for (ResourceValue value : sizes) { |
| String v = value.getValue(); |
| if (v != null) { |
| size = v; |
| if (v.startsWith("0")) { |
| break; |
| } |
| } |
| } |
| } |
| } |
| if (!size.startsWith("0")) { //$NON-NLS-1$ |
| String msg = String.format( |
| "Use a `%1$s` of `0dp` instead of `%2$s` for better performance", |
| dimension, size); |
| context.report(INEFFICIENT_WEIGHT, |
| weightChild, |
| context.getLocation(sizeNode != null ? sizeNode : weightChild), msg); |
| |
| } |
| } |
| |
| if (context.isEnabled(WRONG_0DP)) { |
| checkWrong0Dp(context, element, children); |
| } |
| } |
| |
| private static void checkWrong0Dp(XmlContext context, Element element, |
| List<Element> children) { |
| boolean isVertical = false; |
| String orientation = element.getAttributeNS(ANDROID_URI, ATTR_ORIENTATION); |
| if (VALUE_VERTICAL.equals(orientation)) { |
| isVertical = true; |
| } |
| |
| for (Element child : children) { |
| String tagName = child.getTagName(); |
| if (tagName.equals(VIEW)) { |
| // Might just used for spacing |
| return; |
| } |
| if (tagName.indexOf('.') != -1 || tagName.equals(VIEW_TAG)) { |
| // Custom views might perform their own dynamic sizing or ignore the layout |
| // attributes all together |
| return; |
| } |
| |
| boolean hasWeight = child.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_WEIGHT); |
| |
| Attr widthNode = child.getAttributeNodeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH); |
| Attr heightNode = child.getAttributeNodeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT); |
| |
| boolean noWidth = false; |
| boolean noHeight = false; |
| if (widthNode != null && widthNode.getValue().startsWith("0")) { //$NON-NLS-1$ |
| noWidth = true; |
| } |
| if (heightNode != null && heightNode.getValue().startsWith("0")) { //$NON-NLS-1$ |
| noHeight = true; |
| } else if (!noWidth) { |
| return; |
| } |
| |
| // If you're specifying 0dp for both the width and height you are probably |
| // trying to hide it deliberately |
| if (noWidth && noHeight) { |
| return; |
| } |
| |
| if (noWidth) { |
| if (!hasWeight) { |
| context.report(WRONG_0DP, widthNode, context.getLocation(widthNode), |
| "Suspicious size: this will make the view invisible, should be " + |
| "used with `layout_weight`"); |
| } else if (isVertical) { |
| context.report(WRONG_0DP, widthNode, context.getLocation(widthNode), |
| "Suspicious size: this will make the view invisible, probably " + |
| "intended for `layout_height`"); |
| } |
| } else { |
| if (!hasWeight) { |
| context.report(WRONG_0DP, widthNode, context.getLocation(heightNode), |
| "Suspicious size: this will make the view invisible, should be " + |
| "used with `layout_weight`"); |
| } else if (!isVertical) { |
| context.report(WRONG_0DP, widthNode, context.getLocation(heightNode), |
| "Suspicious size: this will make the view invisible, probably " + |
| "intended for `layout_width`"); |
| } |
| } |
| } |
| } |
| } |