blob: d82478bb79cc74e3d3d6ab5aa4f0836c987efe92 [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.ABSOLUTE_LAYOUT;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_BACKGROUND;
import static com.android.SdkConstants.ATTR_ID;
import static com.android.SdkConstants.ATTR_PADDING;
import static com.android.SdkConstants.ATTR_PADDING_BOTTOM;
import static com.android.SdkConstants.ATTR_PADDING_END;
import static com.android.SdkConstants.ATTR_PADDING_LEFT;
import static com.android.SdkConstants.ATTR_PADDING_RIGHT;
import static com.android.SdkConstants.ATTR_PADDING_START;
import static com.android.SdkConstants.ATTR_PADDING_TOP;
import static com.android.SdkConstants.ATTR_STYLE;
import static com.android.SdkConstants.FRAME_LAYOUT;
import static com.android.SdkConstants.GRID_LAYOUT;
import static com.android.SdkConstants.GRID_VIEW;
import static com.android.SdkConstants.HORIZONTAL_SCROLL_VIEW;
import static com.android.SdkConstants.LINEAR_LAYOUT;
import static com.android.SdkConstants.RADIO_GROUP;
import static com.android.SdkConstants.RELATIVE_LAYOUT;
import static com.android.SdkConstants.SCROLL_VIEW;
import static com.android.SdkConstants.TABLE_LAYOUT;
import static com.android.SdkConstants.TABLE_ROW;
import static com.android.SdkConstants.VIEW_MERGE;
import com.android.annotations.NonNull;
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.Location;
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.Element;
import org.w3c.dom.Node;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* Checks whether the current node can be removed without affecting the layout.
*/
public class UselessViewDetector extends LayoutDetector {
private static final Implementation IMPLEMENTATION = new Implementation(
UselessViewDetector.class,
Scope.RESOURCE_FILE_SCOPE);
/** Issue of including a parent that has no value on its own */
public static final Issue USELESS_PARENT = Issue.create(
"UselessParent", //$NON-NLS-1$
"Useless parent layout",
"A layout with children that has no siblings, is not a scrollview or " +
"a root layout, and does not have a background, can be removed and have " +
"its children moved directly into the parent for a flatter and more " +
"efficient layout hierarchy.",
Category.PERFORMANCE,
2,
Severity.WARNING,
IMPLEMENTATION);
/** Issue of including a leaf that isn't shown */
public static final Issue USELESS_LEAF = Issue.create(
"UselessLeaf", //$NON-NLS-1$
"Useless leaf layout",
"A layout that has no children or no background can often be removed (since it " +
"is invisible) for a flatter and more efficient layout hierarchy.",
Category.PERFORMANCE,
2,
Severity.WARNING,
IMPLEMENTATION);
/** Constructs a new {@link UselessViewDetector} */
public UselessViewDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
private static final List<String> CONTAINERS = new ArrayList<String>(18);
static {
CONTAINERS.add(ABSOLUTE_LAYOUT);
CONTAINERS.add(FRAME_LAYOUT);
CONTAINERS.add(GRID_LAYOUT);
CONTAINERS.add(GRID_VIEW);
CONTAINERS.add(HORIZONTAL_SCROLL_VIEW);
CONTAINERS.add("ImageSwitcher"); //$NON-NLS-1$
CONTAINERS.add(LINEAR_LAYOUT);
CONTAINERS.add(RADIO_GROUP);
CONTAINERS.add(RELATIVE_LAYOUT);
CONTAINERS.add(SCROLL_VIEW);
CONTAINERS.add("SlidingDrawer"); //$NON-NLS-1$
CONTAINERS.add("StackView"); //$NON-NLS-1$
CONTAINERS.add(TABLE_LAYOUT);
CONTAINERS.add(TABLE_ROW);
CONTAINERS.add("TextSwitcher"); //$NON-NLS-1$
CONTAINERS.add("ViewAnimator"); //$NON-NLS-1$
CONTAINERS.add("ViewFlipper"); //$NON-NLS-1$
CONTAINERS.add("ViewSwitcher"); //$NON-NLS-1$
// Available ViewGroups that are not included by this check:
// CONTAINERS.add("android.gesture.GestureOverlayView");
// CONTAINERS.add("AdapterViewFlipper");
// CONTAINERS.add("DialerFilter");
// CONTAINERS.add("ExpandableListView");
// CONTAINERS.add("ListView");
// CONTAINERS.add("MediaController");
// CONTAINERS.add("merge");
// CONTAINERS.add("SearchView");
// CONTAINERS.add("TabWidget");
// CONTAINERS.add("TabHost");
}
@Override
public Collection<String> getApplicableElements() {
return CONTAINERS;
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
int childCount = LintUtils.getChildCount(element);
if (childCount == 0) {
// Check to see if this is a leaf layout that can be removed
checkUselessLeaf(context, element);
} else {
// Check to see if this is a middle-man layout which can be removed
checkUselessMiddleLayout(context, element);
}
}
// This is the old UselessLayoutCheck from layoutopt
private static void checkUselessMiddleLayout(XmlContext context, Element element) {
// Conditions:
// - The node has children
// - The node does not have siblings
// - The node's parent is not a scroll view (horizontal or vertical)
// - The node does not have a background or its parent does not have a
// background or neither the node and its parent have a background
// - The parent is not a <merge/>
Node parentNode = element.getParentNode();
if (parentNode.getNodeType() != Node.ELEMENT_NODE) {
// Can't remove root
return;
}
Element parent = (Element) parentNode;
String parentTag = parent.getTagName();
if (parentTag.equals(SCROLL_VIEW) || parentTag.equals(HORIZONTAL_SCROLL_VIEW) ||
parentTag.equals(VIEW_MERGE)) {
// Can't remove if the parent is a scroll view or a merge
return;
}
// This method is only called when we've already ensured that it has children
assert LintUtils.getChildCount(element) > 0;
int parentChildCount = LintUtils.getChildCount(parent);
if (parentChildCount != 1) {
// Don't remove if the node has siblings
return;
}
// - A parent can be removed if it doesn't have a background
// - A parent can be removed if has a background *and* the child does not have a
// background (in which case, just move the background over to the child, remove
// the parent)
// - If both child and parent have a background, the parent cannot be removed (a
// background can be translucent, have transparent padding, etc.)
boolean nodeHasBackground = element.hasAttributeNS(ANDROID_URI, ATTR_BACKGROUND);
boolean parentHasBackground = parent.hasAttributeNS(ANDROID_URI, ATTR_BACKGROUND);
if (nodeHasBackground && parentHasBackground) {
// Can't remove because both define a background, and they might both be
// visible (e.g. through transparency or padding).
return;
}
// Certain parents are special - such as the TabHost and the GestureOverlayView -
// where we want to leave things alone.
if (!CONTAINERS.contains(parentTag)) {
return;
}
// If we define a padding, and the parent provides a background, then
// this view is not *necessarily* useless.
if (parentHasBackground && element.hasAttributeNS(ANDROID_URI, ATTR_PADDING)
|| element.hasAttributeNS(ANDROID_URI, ATTR_PADDING_LEFT)
|| element.hasAttributeNS(ANDROID_URI, ATTR_PADDING_RIGHT)
|| element.hasAttributeNS(ANDROID_URI, ATTR_PADDING_TOP)
|| element.hasAttributeNS(ANDROID_URI, ATTR_PADDING_BOTTOM)
|| element.hasAttributeNS(ANDROID_URI, ATTR_PADDING_START)
|| element.hasAttributeNS(ANDROID_URI, ATTR_PADDING_END)) {
return;
}
boolean hasId = element.hasAttributeNS(ANDROID_URI, ATTR_ID);
Location location = context.getLocation(element);
String tag = element.getTagName();
String format;
if (hasId) {
format = "This `%1$s` layout or its `%2$s` parent is possibly useless";
} else {
format = "This `%1$s` layout or its `%2$s` parent is useless";
}
if (nodeHasBackground || parentHasBackground) {
format += "; transfer the `background` attribute to the other view";
}
String message = String.format(format, tag, parentTag);
context.report(USELESS_PARENT, element, location, message);
}
// This is the old UselessView check from layoutopt
private static void checkUselessLeaf(XmlContext context, Element element) {
assert LintUtils.getChildCount(element) == 0;
// Conditions:
// - The node is a container view (LinearLayout, etc.)
// - The node has no id
// - The node has no background
// - The node has no children
// - The node has no style
// - The node is not a root
if (element.hasAttributeNS(ANDROID_URI, ATTR_ID)) {
return;
}
if (element.hasAttributeNS(ANDROID_URI, ATTR_BACKGROUND)) {
return;
}
if (element.hasAttribute(ATTR_STYLE)) {
return;
}
if (element == context.document.getDocumentElement()) {
return;
}
Location location = context.getLocation(element);
String tag = element.getTagName();
String message = String.format(
"This `%1$s` view is useless (no children, no `background`, no `id`, no `style`)", tag);
context.report(USELESS_LEAF, element, location, message);
}
}