| /* |
| * 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.SPACE; |
| import static com.android.SdkConstants.VALUE_FALSE; |
| import static com.android.SdkConstants.VALUE_FILL_PARENT; |
| import static com.android.SdkConstants.VALUE_HORIZONTAL; |
| 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.Lint; |
| import com.android.tools.lint.detector.api.LintFix; |
| import com.android.tools.lint.detector.api.Scope; |
| import com.android.tools.lint.detector.api.Severity; |
| import com.android.tools.lint.detector.api.XmlContext; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.IdentityHashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import org.w3c.dom.Attr; |
| import org.w3c.dom.Element; |
| import org.w3c.dom.Node; |
| |
| /** 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", |
| "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", |
| "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", |
| "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", |
| "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", |
| "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<>(); |
| |
| /** Constructs a new {@link InefficientWeightDetector} */ |
| public InefficientWeightDetector() {} |
| |
| @Override |
| public Collection<String> getApplicableElements() { |
| return Collections.singletonList(LINEAR_LAYOUT); |
| } |
| |
| @Override |
| public void visitElement(@NonNull XmlContext context, @NonNull Element element) { |
| List<Element> children = Lint.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.getNameLocation(element), |
| message, |
| createOrientationFixes()); |
| } |
| } 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 = |
| Lint.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.getNameLocation(element), |
| message, |
| createOrientationFixes()); |
| } |
| } |
| |
| 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) { |
| LintFix fix = |
| fix().set(ANDROID_URI, ATTR_BASELINE_ALIGNED, VALUE_FALSE) |
| .autoFix() |
| .build(); |
| context.report( |
| BASELINE_WEIGHTS, |
| element, |
| context.getNameLocation(element), |
| "Set `android:baselineAligned=\"false\"` on this element for better performance", |
| fix); |
| } |
| } |
| |
| 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 = |
| Lint.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")) { |
| 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.getElementLocation(weightChild, sizeNode, null, null), |
| msg); |
| } |
| } |
| |
| if (context.isEnabled(WRONG_0DP)) { |
| checkWrong0Dp(context, element, children); |
| } |
| } |
| |
| @NonNull |
| private LintFix createOrientationFixes() { |
| LintFix horizontal = |
| fix().name("Set orientation=\"horizontal\" (default)") |
| .set(ANDROID_URI, ATTR_ORIENTATION, VALUE_HORIZONTAL) |
| .build(); |
| LintFix vertical = |
| fix().name("Set orientation=\"vertical\" (changes layout)") |
| .set(ANDROID_URI, ATTR_ORIENTATION, VALUE_VERTICAL) |
| .build(); |
| return fix().alternatives(horizontal, vertical); |
| } |
| |
| 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")) { |
| noWidth = true; |
| } |
| if (heightNode != null && heightNode.getValue().startsWith("0")) { |
| noHeight = true; |
| } else if (!noWidth) { |
| return; |
| } |
| |
| if (child.getTagName().equals(SPACE)) { |
| 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`"); |
| } |
| } |
| } |
| } |
| } |