blob: 36ac6f2b62b71b537e9cbd4dd7404039e545d99c [file] [log] [blame]
/*
* 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`");
}
}
}
}
}