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