blob: cd6312e417ab35d768a772c947c5da02011ea9d2 [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.GALLERY;
import static com.android.SdkConstants.GRID_VIEW;
import static com.android.SdkConstants.HORIZONTAL_SCROLL_VIEW;
import static com.android.SdkConstants.LIST_VIEW;
import static com.android.SdkConstants.SCROLL_VIEW;
import com.android.annotations.NonNull;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Context;
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.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.XmlContext;
import java.util.Arrays;
import java.util.Collection;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
/** Checks whether a scroll view contains a nested scrolling widget */
public class NestedScrollingWidgetDetector extends LayoutDetector {
private int mVisitingHorizontalScroll;
private int mVisitingVerticalScroll;
/** The main issue discovered by this detector */
public static final Issue ISSUE =
Issue.create(
"NestedScrolling",
"Nested scrolling widgets",
// TODO: Better description!
"A scrolling widget such as a `ScrollView` should not contain any nested "
+ "scrolling widgets since this has various usability issues",
Category.CORRECTNESS,
7,
Severity.WARNING,
new Implementation(
NestedScrollingWidgetDetector.class, Scope.RESOURCE_FILE_SCOPE));
/** Constructs a new {@link NestedScrollingWidgetDetector} */
public NestedScrollingWidgetDetector() {}
@Override
public void beforeCheckFile(@NonNull Context context) {
mVisitingHorizontalScroll = 0;
mVisitingVerticalScroll = 0;
}
@Override
@NonNull
public Collection<String> getApplicableElements() {
return Arrays.asList(
SCROLL_VIEW,
LIST_VIEW,
GRID_VIEW,
// Horizontal
GALLERY,
HORIZONTAL_SCROLL_VIEW);
}
private Element findOuterScrollingWidget(Node node, boolean vertical) {
Collection<String> applicableElements = getApplicableElements();
while (node != null) {
if (node instanceof Element) {
Element element = (Element) node;
String tagName = element.getTagName();
if (applicableElements.contains(tagName) && vertical == isVerticalScroll(element)) {
return element;
}
}
node = node.getParentNode();
}
return null;
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
boolean vertical = isVerticalScroll(element);
if (vertical) {
mVisitingVerticalScroll++;
} else {
mVisitingHorizontalScroll++;
}
if (mVisitingHorizontalScroll > 1 || mVisitingVerticalScroll > 1) {
Element parent = findOuterScrollingWidget(element.getParentNode(), vertical);
if (parent != null) {
String format;
if (mVisitingVerticalScroll > 1) {
format =
"The vertically scrolling `%1$s` should not contain another "
+ "vertically scrolling widget (`%2$s`)";
} else {
format =
"The horizontally scrolling `%1$s` should not contain another "
+ "horizontally scrolling widget (`%2$s`)";
}
String msg = String.format(format, parent.getTagName(), element.getTagName());
context.report(ISSUE, element, context.getElementLocation(element), msg);
}
}
}
@Override
public void visitElementAfter(@NonNull XmlContext context, @NonNull Element element) {
if (isVerticalScroll(element)) {
mVisitingVerticalScroll--;
assert mVisitingVerticalScroll >= 0;
} else {
mVisitingHorizontalScroll--;
assert mVisitingHorizontalScroll >= 0;
}
}
private static boolean isVerticalScroll(Element element) {
String view = element.getTagName();
if (view.equals(GALLERY) || view.equals(HORIZONTAL_SCROLL_VIEW)) {
return false;
} else {
// This method should only be called with one of the 5 widget types
// listed in getApplicableElements
assert view.equals(SCROLL_VIEW) || view.equals(LIST_VIEW) || view.equals(GRID_VIEW);
return true;
}
}
}