blob: 13f89aa6c75443cbaaddc3126cc8e141c9588456 [file] [log] [blame]
/*
* Copyright (C) 2013 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.idea.rendering;
import com.android.ide.common.rendering.api.ViewInfo;
import com.google.common.collect.Lists;
import com.intellij.psi.PsiFile;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.xml.XmlTag;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
/**
* The {@linkplain com.android.tools.idea.rendering.RenderedViewHierarchy} builds up and queries a hierarchy of
* {@link com.android.tools.idea.rendering.RenderedView}, which corresponds to a {@link com.android.ide.common.rendering.api.ViewInfo}
* tree returned from layoutlib. In addition to building it up, it provides methods to find views corresponding to {@code x,y} coordinates
* as well as to find the corresponding bounds for views, identified by {@link XmlTag} elements.
*/
public class RenderedViewHierarchy {
private final List<RenderedView> myRoots;
private final PsiFile myFile;
private final List<RenderedView> myIncludedRoots;
private RenderedViewHierarchy(@NotNull PsiFile file, @NotNull List<RenderedView> roots, boolean computeIncludeBounds) {
myFile = file;
myRoots = roots;
if (computeIncludeBounds) {
myIncludedRoots = Lists.newArrayList();
for (RenderedView root : myRoots) {
addIncludedBounds(root);
}
} else {
myIncludedRoots = null;
}
}
@NotNull
public static RenderedViewHierarchy create(@NotNull PsiFile file, @NotNull List<ViewInfo> roots, boolean computeIncludeBounds) {
return new RenderedViewHierarchy(file, convert(null, roots, 0, 0), computeIncludeBounds);
}
public List<RenderedView> getRoots() {
return myRoots;
}
/**
* Returns the list of root views that were included in a layout that was rendered as included in another. This is typically
* just one view, but can be more than one when the {@code <include/>} tag loads a layout whose root tag is a {@code <merge/>}.
* Can be null when there are no included views.
*
* @return a list of included view roots, or null
*/
@Nullable
public List<RenderedView> getIncludedRoots() {
return myIncludedRoots;
}
@NotNull
private static List<RenderedView> convert(@Nullable RenderedView parent, @NotNull List<ViewInfo> roots, int parentX, int parentY) {
List<RenderedView> views = new ArrayList<RenderedView>(roots.size());
for (ViewInfo info : roots) {
XmlTag tag = null;
Object cookie = info.getCookie();
if (cookie instanceof XmlTag) {
tag = (XmlTag)cookie;
}
int x = info.getLeft();
int y = info.getTop();
int width = info.getRight() - x;
int height = info.getBottom() - y;
x += parentX;
y += parentY;
RenderedView view = new RenderedView(parent, info, tag, x, y, width, height);
List<ViewInfo> children = info.getChildren();
if (children != null && !children.isEmpty()) {
view.setChildren(convert(view, children, x, y));
}
views.add(view);
}
return views;
}
// If I create maps, count views first:
//private static int count(@NotNull List<ViewInfo> views) {
// int count = 0;
// for (ViewInfo info : views) {
// count++;
// List<ViewInfo> children = info.getChildren();
// if (children != null && !children.isEmpty()) {
// count += count(children);
// }
// }
//
// return count;
//}
private void addIncludedBounds(RenderedView view) {
if (view.tag != null) {
myIncludedRoots.add(view);
} else {
for (RenderedView child : view.getChildren()) {
addIncludedBounds(child);
}
}
}
@Nullable
public List<RenderedView> findByOffset(int offset) {
XmlTag tag = PsiTreeUtil.findElementOfClassAtOffset(myFile, offset, XmlTag.class, false);
return (tag != null) ? findViewsByTag(tag) : null;
}
@Nullable
public RenderedView findLeafAt(int x, int y) {
// Search BACKWARDS such that if the children are painted on top of each
// other (as is the case in a FrameLayout) I pick the last one which will
// be topmost!
for (int i = myRoots.size() - 1; i >= 0; i--) {
RenderedView view = myRoots.get(i);
RenderedView leaf = view.findLeafAt(x, y);
if (leaf != null) {
return leaf;
}
}
return null;
}
@Nullable
public RenderedView findViewByTag(@NotNull XmlTag tag) {
// TODO: Consider using lookup map
for (RenderedView view : myRoots) {
RenderedView match = view.findViewByTag(tag);
if (match != null) {
return match;
}
}
return null;
}
@Nullable
public List<RenderedView> findViewsByTag(@NotNull XmlTag tag) {
List<RenderedView> result = null;
for (RenderedView view : myRoots) {
List<RenderedView> matches = view.findViewsByTag(tag);
if (matches != null) {
if (result != null) {
result.addAll(matches);
} else {
result = matches;
}
}
}
return result;
}
}