diff options
6 files changed, 918 insertions, 107 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/BinPacker.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/BinPacker.java new file mode 100644 index 0000000..9fc2e09 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/BinPacker.java @@ -0,0 +1,352 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.ide.eclipse.adt.internal.editors.layout.gle2; + +import com.android.annotations.Nullable; +import com.android.ide.common.api.Rect; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import javax.imageio.ImageIO; + +/** + * This class implements 2D bin packing: packing rectangles into a given area as + * tightly as "possible" (bin packing in general is NP hard, so this class uses + * heuristics). + * <p> + * The algorithm implemented is to keep a set of (possibly overlapping) + * available areas for placement. For each newly inserted rectangle, we first + * pick which available space to occupy, and we then subdivide the + * current rectangle into all the possible remaining unoccupied sub-rectangles. + * We also remove any other space rectangles which are no longer eligible if + * they are intersecting the newly placed rectangle. + * <p> + * This algorithm is not very fast, so should not be used for a large number of + * rectangles. + */ +class BinPacker { + /** + * When enabled, the successive passes are dumped as PNG images showing the + * various available and occupied rectangles) + */ + private static final boolean DEBUG = false; + + private final List<Rect> mSpace = new ArrayList<Rect>(); + private final int mMinHeight; + private final int mMinWidth; + + /** + * Creates a new {@linkplain BinPacker}. To use it, first add one or more + * initial available space rectangles with {@link #addSpace(Rect)}, and then + * place the rectangles with {@link #occupy(int, int)}. The returned + * {@link Rect} from {@link #occupy(int, int)} gives the coordinates of the + * positioned rectangle. + * + * @param minWidth the smallest width of any rectangle placed into this bin + * @param minHeight the smallest height of any rectangle placed into this bin + */ + BinPacker(int minWidth, int minHeight) { + mMinWidth = minWidth; + mMinHeight = minHeight; + + if (DEBUG) { + mAllocated = new ArrayList<Rect>(); + sLayoutId++; + sRectId = 1; + } + } + + /** Adds more available space */ + void addSpace(Rect rect) { + if (rect.w >= mMinWidth && rect.h >= mMinHeight) { + mSpace.add(rect); + } + } + + /** Attempts to place a rectangle of the given dimensions, if possible */ + @Nullable + Rect occupy(int width, int height) { + int index = findBest(width, height); + if (index == -1) { + return null; + } + + return split(index, width, height); + } + + /** + * Finds the best available space rectangle to position a new rectangle of + * the given size in. + */ + private int findBest(int width, int height) { + if (mSpace.isEmpty()) { + return -1; + } + + // Try to pack as far up as possible first + int bestIndex = -1; + boolean multipleAtSameY = false; + int minY = Integer.MAX_VALUE; + for (int i = 0, n = mSpace.size(); i < n; i++) { + Rect rect = mSpace.get(i); + if (rect.y <= minY) { + if (rect.w >= width && rect.h >= height) { + if (rect.y < minY) { + minY = rect.y; + multipleAtSameY = false; + bestIndex = i; + } else if (minY == rect.y) { + multipleAtSameY = true; + } + } + } + } + + if (!multipleAtSameY) { + return bestIndex; + } + + bestIndex = -1; + + // Pick a rectangle. This currently tries to find the rectangle whose shortest + // side most closely matches the placed rectangle's size. + // Attempt to find the best short side fit + int bestShortDistance = Integer.MAX_VALUE; + int bestLongDistance = Integer.MAX_VALUE; + + for (int i = 0, n = mSpace.size(); i < n; i++) { + Rect rect = mSpace.get(i); + if (rect.y != minY) { // Only comparing elements at same y + continue; + } + if (rect.w >= width && rect.h >= height) { + if (width < height) { + int distance = rect.w - width; + if (distance < bestShortDistance || + distance == bestShortDistance && + (rect.h - height) < bestLongDistance) { + bestShortDistance = distance; + bestLongDistance = rect.h - height; + bestIndex = i; + } + } else { + int distance = rect.w - width; + if (distance < bestShortDistance || + distance == bestShortDistance && + (rect.h - height) < bestLongDistance) { + bestShortDistance = distance; + bestLongDistance = rect.h - height; + bestIndex = i; + } + } + } + } + + return bestIndex; + } + + /** + * Removes the rectangle at the given index. Since the rectangles are in an + * {@link ArrayList}, removing a rectangle in the normal way is slow (it + * would involve shifting all elements), but since we don't care about + * order, this always swaps the to-be-deleted element to the last position + * in the array first, <b>then</b> it deletes it (which should be + * immediate). + * + * @param index the index in the {@link #mSpace} list to remove a rectangle + * from + */ + private void removeRect(int index) { + assert !mSpace.isEmpty(); + int lastIndex = mSpace.size() - 1; + if (index != lastIndex) { + // Swap before remove to make deletion faster since we don't + // care about order + Rect temp = mSpace.get(index); + mSpace.set(index, mSpace.get(lastIndex)); + mSpace.set(lastIndex, temp); + } + + mSpace.remove(lastIndex); + } + + /** + * Splits the rectangle at the given rectangle index such that it can contain + * a rectangle of the given width and height. */ + private Rect split(int index, int width, int height) { + Rect rect = mSpace.get(index); + assert rect.w >= width && rect.h >= height : rect; + + Rect r = new Rect(rect); + r.w = width; + r.h = height; + + // Remove all rectangles that intersect my rectangle + for (int i = 0; i < mSpace.size(); i++) { + Rect other = mSpace.get(i); + if (other.intersects(r)) { + removeRect(i); + i--; + } + } + + + // Split along vertical line x = rect.x + width: + // (rect.x,rect.y) + // +-------------+-------------------------+ + // | | | + // | | | + // | | height | + // | | | + // | | | + // +-------------+ B | rect.h + // | width | + // | | | + // | A | + // | | | + // | | + // +---------------------------------------+ + // rect.w + int remainingHeight = rect.h - height; + int remainingWidth = rect.w - width; + if (remainingHeight >= mMinHeight) { + mSpace.add(new Rect(rect.x, rect.y + height, width, remainingHeight)); + } + if (remainingWidth >= mMinWidth) { + mSpace.add(new Rect(rect.x + width, rect.y, remainingWidth, rect.h)); + } + + // Split along horizontal line y = rect.y + height: + // +-------------+-------------------------+ + // | | | + // | | height | + // | | A | + // | | | + // | | | rect.h + // +-------------+ - - - - - - - - - - - - | + // | width | + // | | + // | B | + // | | + // | | + // +---------------------------------------+ + // rect.w + if (remainingHeight >= mMinHeight) { + mSpace.add(new Rect(rect.x, rect.y + height, rect.w, remainingHeight)); + } + if (remainingWidth >= mMinWidth) { + mSpace.add(new Rect(rect.x + width, rect.y, remainingWidth, height)); + } + + // Remove redundant rectangles. This is not very efficient. + for (int i = 0; i < mSpace.size() - 1; i++) { + for (int j = i + 1; j < mSpace.size(); j++) { + Rect iRect = mSpace.get(i); + Rect jRect = mSpace.get(j); + if (jRect.contains(iRect)) { + removeRect(i); + i--; + break; + } + if (iRect.contains(jRect)) { + removeRect(j); + j--; + } + } + } + + if (DEBUG) { + mAllocated.add(r); + dumpImage(); + } + + return r; + } + + // DEBUGGING CODE: Enable with DEBUG + + private List<Rect> mAllocated; + private static int sLayoutId; + private static int sRectId; + private void dumpImage() { + if (DEBUG) { + int width = 100; + int height = 100; + for (Rect rect : mSpace) { + width = Math.max(width, rect.w); + height = Math.max(height, rect.h); + } + width += 10; + height += 10; + + BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + Graphics2D g = image.createGraphics(); + g.setColor(Color.BLACK); + g.fillRect(0, 0, image.getWidth(), image.getHeight()); + + Color[] colors = new Color[] { + Color.blue, Color.cyan, + Color.green, Color.magenta, Color.orange, + Color.pink, Color.red, Color.white, Color.yellow, Color.darkGray, + Color.lightGray, Color.gray, + }; + + char allocated = 'A'; + for (Rect rect : mAllocated) { + Color color = new Color(0x9FFFFFFF, true); + g.setColor(color); + g.setBackground(color); + g.fillRect(rect.x, rect.y, rect.w, rect.h); + g.setColor(Color.WHITE); + g.drawRect(rect.x, rect.y, rect.w, rect.h); + g.drawString("" + (allocated++), + rect.x + rect.w / 2, rect.y + rect.h / 2); + } + + int colorIndex = 0; + for (Rect rect : mSpace) { + Color color = colors[colorIndex]; + colorIndex = (colorIndex + 1) % colors.length; + + color = new Color(color.getRed(), color.getGreen(), color.getBlue(), 128); + g.setColor(color); + + g.fillRect(rect.x, rect.y, rect.w, rect.h); + g.setColor(Color.WHITE); + g.drawString(Integer.toString(colorIndex), + rect.x + rect.w / 2, rect.y + rect.h / 2); + } + + + g.dispose(); + + File file = new File("/tmp/layout" + sLayoutId + "_pass" + sRectId + ".png"); + try { + ImageIO.write(image, "PNG", file); + System.out.println("Wrote diagnostics image " + file); + } catch (IOException e) { + e.printStackTrace(); + } + sRectId++; + } + } +}
\ No newline at end of file diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageUtils.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageUtils.java index aeafa6d..b5bc9aa 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageUtils.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageUtils.java @@ -671,8 +671,14 @@ public class ImageUtils { null); } + /** + * Reads the given image from the plugin folder + * + * @param name the name of the image (including file extension) + * @return the corresponding image, or null if something goes wrong + */ @Nullable - private static BufferedImage readImage(@NonNull String name) { + public static BufferedImage readImage(@NonNull String name) { InputStream stream = ImageUtils.class.getResourceAsStream("/icons/" + name); //$NON-NLS-1$ if (stream != null) { try { diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreview.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreview.java index cb55b59..e3a68b8 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreview.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreview.java @@ -59,6 +59,7 @@ import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; import com.android.ide.eclipse.adt.internal.sdk.Sdk; import com.android.ide.eclipse.adt.io.IFileWrapper; import com.android.io.IAbstractFile; +import com.android.resources.Density; import com.android.resources.ResourceFolderType; import com.android.resources.ResourceType; import com.android.resources.ScreenOrientation; @@ -85,8 +86,10 @@ import org.eclipse.ui.PlatformUI; import org.eclipse.ui.progress.UIJob; import org.w3c.dom.Document; +import java.awt.Graphics2D; import java.awt.image.BufferedImage; import java.io.File; +import java.util.Comparator; import java.util.Map; /** @@ -152,6 +155,7 @@ public class RenderPreview implements IJobChangeListener { private int mX; private int mY; private double mScale = 1.0; + private double mAspectRatio; /** If non null, points to a separate file containing the source */ private @Nullable IFile mInput; @@ -203,6 +207,7 @@ public class RenderPreview implements IJobChangeListener { mConfiguration = configuration; mWidth = width; mHeight = height; + mAspectRatio = mWidth / (double) mHeight; } /** @@ -220,15 +225,20 @@ public class RenderPreview implements IJobChangeListener { * @param scale the factor to scale the thumbnail picture by */ public void setScale(double scale) { - Image thumbnail = mThumbnail; - mThumbnail = null; - if (thumbnail != null) { - thumbnail.dispose(); - } + disposeThumbnail(); mScale = scale; } /** + * Returns the aspect ratio of this render preview + * + * @return the aspect ratio + */ + public double getAspectRatio() { + return mAspectRatio; + } + + /** * Returns whether the preview is actively hovered * * @return whether the mouse is hovering over the preview @@ -277,6 +287,15 @@ public class RenderPreview implements IJobChangeListener { } /** + * Returns the area of this render preview, PRIOR to scaling + * + * @return the area (width times height without scaling) + */ + int getArea() { + return mWidth * mHeight; + } + + /** * Sets whether the preview is visible. Previews that are off * screen are typically marked invisible during layout, which means we don't * have to expend effort computing preview thumbnails etc @@ -289,10 +308,13 @@ public class RenderPreview implements IJobChangeListener { if (mVisible) { if (mDirty != 0) { // Just made the render preview visible: - configurationChanged(mDirty); + configurationChanged(mDirty); // schedules render } else { updateForkStatus(); + mManager.scheduleRender(this); } + } else { + dispose(); } } } @@ -404,10 +426,7 @@ public class RenderPreview implements IJobChangeListener { * of image resources etc */ public void dispose() { - if (mThumbnail != null) { - mThumbnail.dispose(); - mThumbnail = null; - } + disposeThumbnail(); if (mJob != null) { mJob.cancel(); @@ -415,6 +434,14 @@ public class RenderPreview implements IJobChangeListener { } } + /** Disposes the thumbnail rendering. */ + void disposeThumbnail() { + if (mThumbnail != null) { + mThumbnail.dispose(); + mThumbnail = null; + } + } + /** * Returns the display name of this preview * @@ -482,10 +509,7 @@ public class RenderPreview implements IJobChangeListener { /** Render immediately */ private void renderSync() { - if (mThumbnail != null) { - mThumbnail.dispose(); - mThumbnail = null; - } + disposeThumbnail(); GraphicalEditorPart editor = mCanvas.getEditorDelegate().getGraphicalEditor(); ResourceResolver resolver = getResourceResolver(); @@ -522,6 +546,7 @@ public class RenderPreview implements IJobChangeListener { Document document = DomUtilities.getDocument(mInput); if (document == null) { mError = true; + createErrorThumbnail(); return; } model.loadFromXmlNode(document); @@ -558,9 +583,13 @@ public class RenderPreview implements IJobChangeListener { if (render.isSuccess()) { BufferedImage image = session.getImage(); if (image != null) { - setFullImage(image); + createThumbnail(image); } } + + if (mError) { + createErrorThumbnail(); + } } private ResourceResolver getResourceResolver() { @@ -626,13 +655,12 @@ public class RenderPreview implements IJobChangeListener { * * @param image the full size image */ - void setFullImage(BufferedImage image) { + void createThumbnail(BufferedImage image) { if (image == null) { mThumbnail = null; return; } - //double scale = getScale(image); double scale = getWidth() / (double) image.getWidth(); if (scale < 1.0) { if (LARGE_SHADOWS) { @@ -664,6 +692,33 @@ public class RenderPreview implements IJobChangeListener { true /* transferAlpha */, -1); } + void createErrorThumbnail() { + int shadowSize = LARGE_SHADOWS ? SHADOW_SIZE : SMALL_SHADOW_SIZE; + int width = getWidth(); + int height = getHeight(); + BufferedImage image = new BufferedImage(width + shadowSize, height + shadowSize, + BufferedImage.TYPE_INT_ARGB); + + Graphics2D g = image.createGraphics(); + g.setColor(java.awt.Color.WHITE); + g.fillRect(0, 0, width, height); + + g.dispose(); + + if (LARGE_SHADOWS) { + ImageUtils.drawRectangleShadow(image, 0, 0, + image.getWidth() - SHADOW_SIZE, + image.getHeight() - SHADOW_SIZE); + } else { + ImageUtils.drawSmallRectangleShadow(image, 0, 0, + image.getWidth() - SMALL_SHADOW_SIZE, + image.getHeight() - SMALL_SHADOW_SIZE); + } + + mThumbnail = SwtUtils.convertToSwt(mCanvas.getDisplay(), image, + true /* transferAlpha */, -1); + } + private static double getScale(int width, int height) { int maxWidth = RenderPreviewManager.getMaxWidth(); int maxHeight = RenderPreviewManager.getMaxHeight(); @@ -685,7 +740,7 @@ public class RenderPreview implements IJobChangeListener { * @return the width in pixels */ public int getWidth() { - return (int) (mScale * mWidth); + return (int) (mWidth * mScale * RenderPreviewManager.getScale()); } /** @@ -694,7 +749,7 @@ public class RenderPreview implements IJobChangeListener { * @return the height in pixels */ public int getHeight() { - return (int) (mScale * mHeight); + return (int) (mHeight * mScale * RenderPreviewManager.getScale()); } /** @@ -774,35 +829,57 @@ public class RenderPreview implements IJobChangeListener { * @param y the y coordinate to paint the preview at */ void paint(GC gc, int x, int y) { - if (mThumbnail != null) { + int width = getWidth(); + int height = getHeight(); + if (mThumbnail != null && !mError) { gc.drawImage(mThumbnail, x, y); if (mActive) { int oldWidth = gc.getLineWidth(); gc.setLineWidth(3); gc.setForeground(gc.getDevice().getSystemColor(SWT.COLOR_LIST_SELECTION)); - gc.drawRectangle(x - 1, y - 1, getWidth() + 2, getHeight() + 2); + gc.drawRectangle(x - 1, y - 1, width + 2, height + 2); gc.setLineWidth(oldWidth); } } else if (mError) { - gc.setBackground(gc.getDevice().getSystemColor(SWT.COLOR_WIDGET_BORDER)); - gc.drawRectangle(x, y, getWidth(), getHeight()); + if (mThumbnail != null) { + gc.drawImage(mThumbnail, x, y); + } else { + gc.setBackground(gc.getDevice().getSystemColor(SWT.COLOR_WIDGET_BORDER)); + gc.drawRectangle(x, y, width, height); + } + + gc.setClipping(x, y, width, height + 100); Image icon = IconFactory.getInstance().getIcon("renderError"); //$NON-NLS-1$ ImageData data = icon.getImageData(); int prevAlpha = gc.getAlpha(); - gc.setAlpha(128-32); - gc.drawImage(icon, x + (getWidth() - data.width) / 2, - y + (getHeight() - data.height) / 2); + int alpha = 128-32; + if (mThumbnail != null) { + alpha -= 64; + } + gc.setAlpha(alpha); + gc.drawImage(icon, x + (width - data.width) / 2, y + (height - data.height) / 2); + + Density density = mConfiguration.getDensity(); + if (density == Density.TV || density == Density.LOW) { + gc.setAlpha(255); + gc.setForeground(gc.getDevice().getSystemColor(SWT.COLOR_BLACK)); + gc.drawText("Broken rendering\nlibrary;\nunsupported DPI\n\nTry updating\nSDK platforms", + x + 8, y + HEADER_HEIGHT, true); + } + gc.setAlpha(prevAlpha); + gc.setClipping((Region) null); } else { gc.setBackground(gc.getDevice().getSystemColor(SWT.COLOR_WIDGET_BORDER)); - gc.drawRectangle(x, y, getWidth(), getHeight()); + gc.drawRectangle(x, y, width, height); + Image icon = IconFactory.getInstance().getIcon("refreshPreview"); //$NON-NLS-1$ ImageData data = icon.getImageData(); int prevAlpha = gc.getAlpha(); gc.setAlpha(128-32); - gc.drawImage(icon, x + (getWidth() - data.width) / 2, - y + (getHeight() - data.height) / 2); + gc.drawImage(icon, x + (width - data.width) / 2, + y + (height - data.height) / 2); gc.setAlpha(prevAlpha); } @@ -812,7 +889,7 @@ public class RenderPreview implements IJobChangeListener { gc.setAlpha(128+32); Color bg = mCanvas.getDisplay().getSystemColor(SWT.COLOR_WHITE); gc.setBackground(bg); - gc.fillRectangle(left, y, x + getWidth() - left, HEADER_HEIGHT); + gc.fillRectangle(left, y, x + width - left, HEADER_HEIGHT); gc.setAlpha(prevAlpha); // Paint icons @@ -935,9 +1012,12 @@ public class RenderPreview implements IJobChangeListener { int temp = mHeight; mHeight = mWidth; mWidth = temp; + mAspectRatio = mWidth / (double) mHeight; } mDirty = 0; + + mManager.scheduleRender(this); } /** @@ -1069,4 +1149,23 @@ public class RenderPreview implements IJobChangeListener { public ConfigurationDescription getDescription() { return mDescription; } + + /** Sorts render previews into increasing aspect ratio order */ + static Comparator<RenderPreview> INCREASING_ASPECT_RATIO = new Comparator<RenderPreview>() { + @Override + public int compare(RenderPreview preview1, RenderPreview preview2) { + return (int) Math.signum(preview1.mAspectRatio - preview2.mAspectRatio); + } + }; + /** Sorts render previews into visual order: row by row, column by column */ + static Comparator<RenderPreview> VISUAL_ORDER = new Comparator<RenderPreview>() { + @Override + public int compare(RenderPreview preview1, RenderPreview preview2) { + int delta = preview1.mY - preview2.mY; + if (delta == 0) { + delta = preview1.mX - preview2.mX; + } + return delta; + } + }; } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreviewManager.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreviewManager.java index 593dbc4..9d36c9a 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreviewManager.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreviewManager.java @@ -22,6 +22,7 @@ import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPre import com.android.annotations.NonNull; import com.android.annotations.Nullable; +import com.android.ide.common.api.Rect; import com.android.ide.common.resources.configuration.DensityQualifier; import com.android.ide.common.resources.configuration.DeviceConfigHelper; import com.android.ide.common.resources.configuration.FolderConfiguration; @@ -29,6 +30,7 @@ import com.android.ide.common.resources.configuration.LanguageQualifier; import com.android.ide.common.resources.configuration.ScreenSizeQualifier; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.AdtUtils; +import com.android.ide.eclipse.adt.internal.editors.IconFactory; import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ComplementingConfiguration; import com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration; import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationChooser; @@ -52,6 +54,7 @@ import org.eclipse.swt.SWT; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.widgets.ScrollBar; @@ -60,20 +63,24 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Set; + /** * Manager for the configuration previews, which handles layout computations, * managing the image buffer cache, etc */ public class RenderPreviewManager { private static double sScale = 1.0; - private static final int RENDER_DELAY = 100; + private static final int RENDER_DELAY = 150; private static final int PREVIEW_VGAP = 18; private static final int PREVIEW_HGAP = 12; private static final int MAX_WIDTH = 200; private static final int MAX_HEIGHT = MAX_WIDTH; + private static final int ZOOM_ICON_WIDTH = 16; + private static final int ZOOM_ICON_HEIGHT = 16; private @Nullable List<RenderPreview> mPreviews; private @Nullable RenderPreviewList mManualList; private final @NonNull LayoutCanvas mCanvas; @@ -87,12 +94,15 @@ public class RenderPreviewManager { private @Nullable RenderPreview mActivePreview; private @Nullable ScrollBarListener mListener; private int mLayoutHeight; - private int mMaxVisibleY; /** Last seen state revision in this {@link RenderPreviewManager}. If less * than {@link #sRevision}, the previews need to be updated on next exposure */ private static int mRevision; /** Current global revision count */ private static int sRevision; + private boolean mNeedLayout; + private boolean mNeedRender; + private boolean mNeedZoom; + private SwapAnimation mAnimation; /** * Creates a {@link RenderPreviewManager} associated with the given canvas @@ -154,19 +164,26 @@ public class RenderPreviewManager { updatedZoom(); } + /** Zooms to 100 (resets zoom) */ + public void zoomReset() { + sScale = 1.0; + updatedZoom(); + mNeedZoom = mNeedLayout = true; + mCanvas.redraw(); + } + private void updatedZoom() { if (hasPreviews()) { for (RenderPreview preview : mPreviews) { - preview.setScale(sScale); + preview.disposeThumbnail(); } RenderPreview preview = mCanvas.getPreview(); if (preview != null) { - preview.setScale(sScale); + preview.disposeThumbnail(); } } - renderPreviews(); - layout(true); + mNeedLayout = mNeedRender = true; mCanvas.redraw(); } @@ -178,6 +195,10 @@ public class RenderPreviewManager { return (int) sScale * MAX_HEIGHT; } + static double getScale() { + return sScale; + } + /** Delete all the previews */ public void deleteManualPreviews() { disposePreviews(); @@ -242,43 +263,28 @@ public class RenderPreviewManager { /** * Layout Algorithm. This sets the {@link RenderPreview#getX()} and - * {@link RenderPreview#getY()} coordinates of all the previews. It also marks - * previews as visible or invisible via {@link RenderPreview#setVisible(boolean)} - * according to their position and the current visible view port in the layout canvas. - * Finally, it also sets the {@code mMaxVisibleY} and {@code mLayoutHeight} fields, - * such that the scrollbars can compute the right scrolled area, and that scrolling - * can cause render refreshes on views that are made visible. - * - * <p> - * Two shapes to fill. The screen is typically wide. When showing a phone, - * I should use all the vertical space; I then show thumbnails on the right. - * When showing the tablet, I need to do something in between. I reserve at least - * 200 pixels either on the right or on the bottom and use the remainder. - * TODO: Look up better algorithms. Optimal space division algorithm. Can prune etc. + * {@link RenderPreview#getY()} coordinates of all the previews. It also + * marks previews as visible or invisible via + * {@link RenderPreview#setVisible(boolean)} according to their position and + * the current visible view port in the layout canvas. Finally, it also sets + * the {@code mLayoutHeight} field, such that the scrollbars can compute the + * right scrolled area, and that scrolling can cause render refreshes on + * views that are made visible. * <p> - * This is not a traditional bin packing problem, because the objects to be packaged - * do not have a fixed size; we can scale them up and down in order to provide an - * "optimal" size. + * This is not a traditional bin packing problem, because the objects to be + * packaged do not have a fixed size; we can scale them up and down in order + * to provide an "optimal" size. * <p> - * See http://en.wikipedia.org/wiki/Packing_problem - * See http://en.wikipedia.org/wiki/Bin_packing_problem - * <p> - * Returns true if the layout changed (so a redraw is desired) + * See http://en.wikipedia.org/wiki/Packing_problem See + * http://en.wikipedia.org/wiki/Bin_packing_problem */ - boolean layout(boolean refresh) { - if (mPreviews == null || mPreviews.isEmpty()) { - return false; - } + void layout(boolean refresh) { + mNeedLayout = false; - if (mListener == null) { - mListener = new ScrollBarListener(); - mCanvas.getVerticalBar().addSelectionListener(mListener); + if (mPreviews == null || mPreviews.isEmpty()) { + return; } - // TODO: Separate layout heuristics for portrait and landscape orientations (though - // it also depends on the dimensions of the canvas window, which determines the - // shape of the leftover space) - int scaledImageWidth = mHScale.getScaledImgSize(); int scaledImageHeight = mVScale.getScaledImgSize(); Rectangle clientArea = mCanvas.getClientArea(); @@ -289,7 +295,7 @@ public class RenderPreviewManager { && clientArea.width == mPrevCanvasWidth && clientArea.height == mPrevCanvasHeight)) { // No change - return false; + return; } mPrevImageWidth = scaledImageWidth; @@ -297,6 +303,41 @@ public class RenderPreviewManager { mPrevCanvasWidth = clientArea.width; mPrevCanvasHeight = clientArea.height; + if (mListener == null) { + mListener = new ScrollBarListener(); + mCanvas.getVerticalBar().addSelectionListener(mListener); + } + + beginRenderScheduling(); + + mLayoutHeight = 0; + + if (previewsHaveIdenticalSize() || fixedOrder()) { + // If all the preview boxes are of identical sizes, or if the order is predetermined, + // just lay them out in rows. + rowLayout(); + } else if (previewsFit()) { + layoutFullFit(); + } else { + rowLayout(); + } + + mCanvas.updateScrollBars(); + } + + /** + * Performs a simple layout where the views are laid out in a row, wrapping + * around the top left canvas image. + */ + private void rowLayout() { + // TODO: Separate layout heuristics for portrait and landscape orientations (though + // it also depends on the dimensions of the canvas window, which determines the + // shape of the leftover space) + + int scaledImageWidth = mHScale.getScaledImgSize(); + int scaledImageHeight = mVScale.getScaledImgSize(); + Rectangle clientArea = mCanvas.getClientArea(); + int availableWidth = clientArea.x + clientArea.width - getX(); int availableHeight = clientArea.y + clientArea.height - getY(); int maxVisibleY = clientArea.y + clientArea.height; @@ -319,7 +360,11 @@ public class RenderPreviewManager { } } - for (RenderPreview preview : mPreviews) { + ArrayList<RenderPreview> aspectOrder = new ArrayList<RenderPreview>(mPreviews); + Collections.sort(aspectOrder, RenderPreview.INCREASING_ASPECT_RATIO); + + + for (RenderPreview preview : aspectOrder) { if (x > 0 && x + preview.getWidth() > availableWidth) { x = rightHandSide; int prevY = y; @@ -362,10 +407,9 @@ public class RenderPreviewManager { preview.setPosition(x, y); - if (y > maxVisibleY) { + if (y > maxVisibleY && maxVisibleY > 0) { preview.setVisible(false); } else if (!preview.isVisible()) { - preview.render(RENDER_DELAY); preview.setVisible(true); } @@ -375,12 +419,119 @@ public class RenderPreviewManager { } mLayoutHeight = nextY; - mMaxVisibleY = maxVisibleY; - mCanvas.updateScrollBars(); + } + + private boolean fixedOrder() { + // Currently, none of the lists have fixed order. Possibly we could + // consider mMode == RenderPreviewMode.CUSTOM to be fixed. + return false; + } + + /** Returns true if all the previews have the same identical size */ + private boolean previewsHaveIdenticalSize() { + if (!hasPreviews()) { + return true; + } + + Iterator<RenderPreview> iterator = mPreviews.iterator(); + RenderPreview first = iterator.next(); + int width = first.getWidth(); + int height = first.getHeight(); + + while (iterator.hasNext()) { + RenderPreview preview = iterator.next(); + if (width != preview.getWidth() || height != preview.getHeight()) { + return false; + } + } return true; } + /** Returns true if all the previews can fully fit in the available space */ + private boolean previewsFit() { + int scaledImageWidth = mHScale.getScaledImgSize(); + int scaledImageHeight = mVScale.getScaledImgSize(); + Rectangle clientArea = mCanvas.getClientArea(); + int availableWidth = clientArea.x + clientArea.width - getX(); + int availableHeight = clientArea.y + clientArea.height - getY(); + int bottomBorder = scaledImageHeight; + int rightHandSide = scaledImageWidth + PREVIEW_HGAP; + + // First see if we can fit everything; if so, we can try to make the layouts + // larger such that they fill up all the available space + long availableArea = rightHandSide * bottomBorder + + availableWidth * (Math.max(0, availableHeight - bottomBorder)); + + long requiredArea = 0; + for (RenderPreview preview : mPreviews) { + // Note: This does not include individual preview scale; the layout + // algorithm itself may be tweaking the scales to fit elements within + // the layout + requiredArea += preview.getArea(); + } + + return requiredArea * sScale < availableArea; + } + + private void layoutFullFit() { + int scaledImageWidth = mHScale.getScaledImgSize(); + int scaledImageHeight = mVScale.getScaledImgSize(); + Rectangle clientArea = mCanvas.getClientArea(); + int availableWidth = clientArea.x + clientArea.width - getX(); + int availableHeight = clientArea.y + clientArea.height - getY(); + int maxVisibleY = clientArea.y + clientArea.height; + int bottomBorder = scaledImageHeight; + int rightHandSide = scaledImageWidth + PREVIEW_HGAP; + + int minWidth = Integer.MAX_VALUE; + int minHeight = Integer.MAX_VALUE; + for (RenderPreview preview : mPreviews) { + minWidth = Math.min(minWidth, preview.getWidth()); + minHeight = Math.min(minHeight, preview.getHeight()); + } + + BinPacker packer = new BinPacker(minWidth, minHeight); + + // TODO: Instead of this, just start with client area and occupy scaled image size! + + // Add in gap on right and bottom since we'll add that requirement on the width and + // height rectangles too (for spacing) + packer.addSpace(new Rect(rightHandSide, 0, + availableWidth - rightHandSide + PREVIEW_HGAP, + availableHeight + PREVIEW_VGAP)); + if (maxVisibleY > bottomBorder) { + packer.addSpace(new Rect(0, bottomBorder + PREVIEW_VGAP, + availableWidth + PREVIEW_HGAP, maxVisibleY - bottomBorder + PREVIEW_VGAP)); + } + + // TODO: Sort previews first before attempting to position them? + + ArrayList<RenderPreview> aspectOrder = new ArrayList<RenderPreview>(mPreviews); + Collections.sort(aspectOrder, RenderPreview.INCREASING_ASPECT_RATIO); + + for (RenderPreview preview : aspectOrder) { + int previewWidth = preview.getWidth(); + int previewHeight = preview.getHeight(); + previewHeight += PREVIEW_VGAP; + if (preview.isForked()) { + previewHeight += PREVIEW_VGAP; + } + previewWidth += PREVIEW_HGAP; + // title height? how do I account for that? + Rect position = packer.occupy(previewWidth, previewHeight); + if (position != null) { + preview.setPosition(position.x, position.y); + preview.setVisible(true); + } else { + // Can't fit: give up and do plain row layout + rowLayout(); + return; + } + } + + mLayoutHeight = availableHeight; + } /** * Paints the configuration previews * @@ -389,7 +540,15 @@ public class RenderPreviewManager { void paint(GC gc) { if (hasPreviews()) { // Ensure up to date at all times; consider moving if it's too expensive - layout(false); + layout(mNeedLayout); + if (mNeedRender) { + renderPreviews(); + } + if (mNeedZoom) { + boolean allowZoomIn = true /*mMode == RenderPreviewMode.NONE*/; + mCanvas.setFitScale(false /*onlyZoomOut*/, allowZoomIn); + mNeedZoom = false; + } int rootX = getX(); int rootY = getY(); @@ -415,6 +574,23 @@ public class RenderPreviewManager { int y = destY + destHeight - preview.getHeight(); preview.paintTitle(gc, x, y, false /*showFile*/); } + + // Zoom overlay + int x = getZoomX(); + if (x > 0) { + int y = getZoomY(); + IconFactory iconFactory = IconFactory.getInstance(); + Image zoomOut = iconFactory.getIcon("zoomminus"); //$NON-NLS-1$); + Image zoomIn = iconFactory.getIcon("zoomplus"); //$NON-NLS-1$); + Image zoom100 = iconFactory.getIcon("zoom100"); //$NON-NLS-1$); + + gc.drawImage(zoomIn, x, y); + y += ZOOM_ICON_HEIGHT; + gc.drawImage(zoomOut, x, y); + y += ZOOM_ICON_HEIGHT; + gc.drawImage(zoom100, x, y); + y += ZOOM_ICON_HEIGHT; + } } else if (mMode == RenderPreviewMode.CUSTOM) { int rootX = getX(); rootX += mHScale.getScaledImgSize(); @@ -426,6 +602,10 @@ public class RenderPreviewManager { gc.drawText("Add previews with \"Add as Thumbnail\"\nin the configuration menu", rootX, rootY, true); } + + if (mAnimation != null) { + mAnimation.tick(gc); + } } private void addPreview(@NonNull RenderPreview preview) { @@ -460,7 +640,8 @@ public class RenderPreviewManager { addPreview(preview); layout(true); - preview.render(RENDER_DELAY); + beginRenderScheduling(); + scheduleRender(preview); mCanvas.setFitScale(true /* onlyZoomOut */, false /*allowZoomIn*/); if (mManualList == null) { @@ -647,11 +828,13 @@ public class RenderPreviewManager { assert false : mMode; } - layout(true); - renderPreviews(); - boolean allowZoomIn = mMode == RenderPreviewMode.NONE; - mCanvas.setFitScale(true /*onlyZoomOut*/, allowZoomIn); - mCanvas.updateScrollBars(); + // We schedule layout for the next redraw rather than process it here immediately; + // not only does this let us avoid doing work for windows where the tab is in the + // background, but when a file is opened we may not know the size of the canvas + // yet, and the layout methods need it in order to do a good job. By the time + // the canvas is painted, we have accurate bounds. + mNeedLayout = mNeedRender = true; + mCanvas.redraw(); return true; } @@ -883,20 +1066,16 @@ public class RenderPreviewManager { public void configurationChanged(int flags) { // Similar to renderPreviews, but only acts on incomplete previews if (hasPreviews()) { - long delay = 0; // Do zoomed images first + beginRenderScheduling(); for (RenderPreview preview : mPreviews) { if (preview.getScale() > 1.2) { preview.configurationChanged(flags); - delay += RENDER_DELAY; - preview.render(delay); } } for (RenderPreview preview : mPreviews) { if (preview.getScale() <= 1.2) { preview.configurationChanged(flags); - delay += RENDER_DELAY; - preview.render(delay); } } RenderPreview preview = mCanvas.getPreview(); @@ -904,7 +1083,7 @@ public class RenderPreviewManager { preview.configurationChanged(flags); preview.dispose(); } - layout(true); + mNeedLayout = true; mCanvas.redraw(); } } @@ -912,22 +1091,49 @@ public class RenderPreviewManager { /** Updates the configuration preview thumbnails */ public void renderPreviews() { if (hasPreviews()) { - long delay = 0; + beginRenderScheduling(); + + // Process in visual order + ArrayList<RenderPreview> visualOrder = new ArrayList<RenderPreview>(mPreviews); + Collections.sort(visualOrder, RenderPreview.VISUAL_ORDER); + // Do zoomed images first - for (RenderPreview preview : mPreviews) { + for (RenderPreview preview : visualOrder) { if (preview.getScale() > 1.2 && preview.isVisible()) { - delay += RENDER_DELAY; - preview.render(delay); + scheduleRender(preview); } } // Non-zoomed images - for (RenderPreview preview : mPreviews) { + for (RenderPreview preview : visualOrder) { if (preview.getScale() <= 1.2 && preview.isVisible()) { - delay += RENDER_DELAY; - preview.render(delay); + scheduleRender(preview); } } } + + mNeedRender = false; + } + + private int mPendingRenderCount; + + /** + * Reset rendering scheduling. The next render request will be scheduled + * after a single delay unit. + */ + public void beginRenderScheduling() { + mPendingRenderCount = 0; + } + + /** + * Schedule rendering the given preview. Each successive call will add an additional + * delay unit to the schedule from the previous {@link #scheduleRender(RenderPreview)} + * call, until {@link #beginRenderScheduling()} is called again. + * + * @param preview the preview to render + */ + public void scheduleRender(@NonNull RenderPreview preview) { + mPendingRenderCount++; + preview.render(mPendingRenderCount * RENDER_DELAY); } /** @@ -961,12 +1167,24 @@ public class RenderPreviewManager { // Switch main editor to the clicked configuration preview mCanvas.setPreview(preview); - chooser.setConfiguration(preview.getConfiguration()); + + Configuration newConfiguration = preview.getConfiguration(); + if (newConfiguration instanceof NestedConfiguration) { + // Should never use a complementing configuration for the main + // rendering's configuration; instead, create a new configuration + // with a snapshot of the configuration's current values + newConfiguration = Configuration.copy(preview.getConfiguration()); + } + chooser.setConfiguration(newConfiguration); + editor.recomputeLayout(); + + // Scroll to the top again, if necessary mCanvas.getVerticalBar().setSelection(mCanvas.getVerticalBar().getMinimum()); - mCanvas.setFitScale(true /*onlyZoomOut*/, false /*allowZoomIn*/); - layout(true); + + mNeedLayout = mNeedZoom = true; mCanvas.redraw(); + mAnimation = new SwapAnimation(preview); } /** @@ -1003,6 +1221,22 @@ public class RenderPreviewManager { return mVScale.translate(0); } + private int getZoomX() { + Rectangle clientArea = mCanvas.getClientArea(); + int x = clientArea.x + clientArea.width - ZOOM_ICON_WIDTH; + if (x < mHScale.getScaledImgSize() + PREVIEW_HGAP) { + // No visible previews because the main image is zoomed too far + return -1; + } + + return x - 6; + } + + private int getZoomY() { + Rectangle clientArea = mCanvas.getClientArea(); + return clientArea.y + 3; + } + /** * Returns the height of the layout * @@ -1062,6 +1296,24 @@ public class RenderPreviewManager { * @return true if the click occurred over a preview and was handled, false otherwise */ public boolean click(ControlPoint mousePos) { + // Clicked zoom? + int x = getZoomX(); + if (x > 0) { + if (mousePos.x >= x && mousePos.x <= x + ZOOM_ICON_WIDTH) { + int y = getZoomY(); + if (mousePos.y >= y && mousePos.y <= y + 3 * ZOOM_ICON_HEIGHT) { + if (mousePos.y < y + ZOOM_ICON_HEIGHT) { + zoomIn(); + } else if (mousePos.y < y + 2 * ZOOM_ICON_HEIGHT) { + zoomOut(); + } else { + zoomReset(); + } + } + } + return true; + } + RenderPreview preview = getPreview(mousePos); if (preview != null) { boolean handled = preview.click(mousePos.x - getX() - preview.getX(), @@ -1160,11 +1412,9 @@ public class RenderPreviewManager { int selection = bar.getSelection(); int thumb = bar.getThumb(); int maxY = selection + thumb; - if (maxY > mMaxVisibleY) { - } + beginRenderScheduling(); for (RenderPreview preview : mPreviews) { if (!preview.isVisible() && preview.getY() <= maxY) { - preview.render(RENDER_DELAY); preview.setVisible(true); } } @@ -1174,4 +1424,51 @@ public class RenderPreviewManager { public void widgetDefaultSelected(SelectionEvent e) { } } + + /** Animation overlay shown briefly after swapping two previews */ + private class SwapAnimation implements Runnable { + private long begin; + private long end; + private static final long DURATION = 400; // ms + private Rect initialPos; + + SwapAnimation(RenderPreview preview) { + begin = System.currentTimeMillis(); + end = begin + DURATION; + + initialPos = new Rect(preview.getX(), preview.getY(), + preview.getWidth(), preview.getHeight()); + } + + void tick(GC gc) { + long now = System.currentTimeMillis(); + if (now > end || mCanvas.isDisposed()) { + mAnimation = null; + return; + } + + // For now, just animation rect1 towards rect2 + // The shape of the canvas might have shifted if zoom or device size + // or orientation changed, so compute a new target size + CanvasTransform hi = mCanvas.getHorizontalTransform(); + CanvasTransform vi = mCanvas.getVerticalTransform(); + Rect rect = new Rect(hi.translate(0), vi.translate(0), + hi.getScaledImgSize(), vi.getScaledImgSize()); + double portion = (now - begin) / (double) DURATION; + rect.x = (int) (portion * (rect.x - initialPos.x) + initialPos.x); + rect.y = (int) (portion * (rect.y - initialPos.y) + initialPos.y); + rect.w = (int) (portion * (rect.w - initialPos.w) + initialPos.w); + rect.h = (int) (portion * (rect.h - initialPos.h) + initialPos.h); + + gc.setForeground(gc.getDevice().getSystemColor(SWT.COLOR_GRAY)); + gc.drawRectangle(rect.x, rect.y, rect.w, rect.h); + + mCanvas.getDisplay().timerExec(5, this); + } + + @Override + public void run() { + mCanvas.redraw(); + } + } } diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/api/RectTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/api/RectTest.java index 784ad8f..2f2c59a 100755 --- a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/api/RectTest.java +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/api/RectTest.java @@ -132,7 +132,13 @@ public class RectTest extends TestCase { public final void testContainsPoint_Null() { // contains(null) returns false rather than an NPE Rect r = new Rect(3, 4, -20, -30); - assertFalse(r.contains(null)); + assertFalse(r.contains((Point) null)); + } + + public final void testContainsRect_Null() { + // contains(null) returns false rather than an NPE + Rect r = new Rect(3, 4, -20, -30); + assertFalse(r.contains((Rect) null)); } public final void testContainsPoint() { @@ -152,6 +158,29 @@ public class RectTest extends TestCase { assertFalse(r.contains(new Point(3, 4+30))); } + public final void testContainsRect() { + Rect r = new Rect(3, 4, 20, 30); + + assertTrue(r.contains(new Rect(3, 4, 5, 10))); + assertFalse(r.contains(new Rect(3 - 1, 4, 5, 10))); + } + + public final void testIntersects() { + Rect r1 = new Rect(0, 0, 10, 10); + Rect r2 = new Rect(1, 1, 5, 5); + Rect r3 = new Rect(10, 0, 1, 1); + Rect r4 = new Rect(5, 5, 10, 10); + Rect r5 = new Rect(-1, 0, 1, 1); + Rect r6 = new Rect(0, 10, 1, 1); + + assertTrue(r1.intersects(r2)); + assertTrue(r2.intersects(r1)); + assertTrue(r1.intersects(r4)); + assertFalse(r1.intersects(r3)); + assertFalse(r1.intersects(r5)); + assertFalse(r1.intersects(r6)); + } + public final void testMoveTo() { Rect r = new Rect(3, 4, 20, 30); Rect r2 = r.moveTo(100, 200); diff --git a/rule_api/src/com/android/ide/common/api/Rect.java b/rule_api/src/com/android/ide/common/api/Rect.java index 0fb791b..88c04a6 100644 --- a/rule_api/src/com/android/ide/common/api/Rect.java +++ b/rule_api/src/com/android/ide/common/api/Rect.java @@ -16,9 +16,9 @@ package com.android.ide.common.api; -import com.google.common.annotations.Beta; import com.android.annotations.NonNull; import com.android.annotations.Nullable; +import com.google.common.annotations.Beta; /** @@ -81,11 +81,39 @@ public class Rect { /** Returns true if the rectangle contains the x,y coordinates, borders included. */ public boolean contains(int x, int y) { - return isValid() && - x >= this.x && - y >= this.y && - x < (this.x + this.w) && - y < (this.y + this.h); + return isValid() + && x >= this.x + && y >= this.y + && x < (this.x + this.w) + && y < (this.y + this.h); + } + + /** + * Returns true if this rectangle intersects the given rectangle. + * Two rectangles intersect if they overlap. + * @param other the other rectangle to test + * @return true if the two rectangles overlap + */ + public boolean intersects(@Nullable Rect other) { + if (other == null) { + return false; + } + if (x2() <= other.x + || other.x2() <= x + || y2() <= other.y + || other.y2() <= y) { + return false; + } + + return true; + } + + /** Returns true if the rectangle fully contains the given rectangle */ + public boolean contains(@Nullable Rect rect) { + return rect != null && x <= rect.x + && y <= rect.y + && x2() >= rect.x2() + && y2() >= rect.y2(); } /** Returns true if the rectangle contains the x,y coordinates, borders included. */ |