aboutsummaryrefslogtreecommitdiffstats
path: root/eclipse
diff options
context:
space:
mode:
authorTor Norbye <tnorbye@google.com>2012-10-10 12:57:27 -0700
committerTor Norbye <tnorbye@google.com>2012-10-12 10:09:34 -0700
commit5ab3464ae51f3169a48c0a70480df2d1af5d9d3a (patch)
treeb0deb5540c74c74f30125b4846ea728917ff579f /eclipse
parent2ff4725103ee97f4d18604d182c5a67e659aa689 (diff)
downloadsdk-5ab3464ae51f3169a48c0a70480df2d1af5d9d3a.zip
sdk-5ab3464ae51f3169a48c0a70480df2d1af5d9d3a.tar.gz
sdk-5ab3464ae51f3169a48c0a70480df2d1af5d9d3a.tar.bz2
Improvements to the multi-configuration layout
This adds a new layout algorithm which tries to do a more optimal fit if all the configuration previews can fit on the current screen without scrolling. (However, it still doesn't scale up these previews to fit all available space, that's coming in a later CL). It also delays rendering previews and performing layout until the layout is actually painted, and improves the error rendering a bit. It's also more deliberate in how preview renderings are scheduled, performing them in visual order etc. There's a new brief animation when you switch to a preview. Finally, there are some preview zoom controls now. Change-Id: Iea503a3fd57dfcaea7656e47b946bfcfea3eecb1
Diffstat (limited to 'eclipse')
-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
5 files changed, 884 insertions, 101 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);