| /* |
| * 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); |
| } |
| } |