aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/BinPacker.java352
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageUtils.java8
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreview.java159
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreviewManager.java435
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/api/RectTest.java31
-rw-r--r--rule_api/src/com/android/ide/common/api/Rect.java40
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. */