diff options
author | Tor Norbye <tnorbye@google.com> | 2010-11-15 09:55:52 -0800 |
---|---|---|
committer | Tor Norbye <tnorbye@google.com> | 2010-11-15 13:38:31 -0800 |
commit | 5d08915fe9014124566a2cdd7c1aa4219e01acea (patch) | |
tree | 2c25da4db7d27b0f86cd62fe6eff490dc16d0317 /eclipse/plugins/com.android.ide.eclipse.adt/src/com | |
parent | 7aa052149875bf58fe9ff3990e6e01140e983afe (diff) | |
download | sdk-5d08915fe9014124566a2cdd7c1aa4219e01acea.zip sdk-5d08915fe9014124566a2cdd7c1aa4219e01acea.tar.gz sdk-5d08915fe9014124566a2cdd7c1aa4219e01acea.tar.bz2 |
Add drop shadow to dragged items
Add in a drop shadow to items dragged from the palette. The primary
reason we need this is that some views (in some themes) only render
light content on top of a transparent background, which makes them
nearly impossible to see on a gray background (e.g. over the palette,
when you initiate the drag). The drop shadow helps add contrast. It
also makes the drag look better since it helps the drag preview image
stand out from the background and visually appear to be lifted on top
of it.
Since we're doing more image processing now, I moved the existing
non-SWT image manipulation methods into a separate ImageUtils class.
This changeset also fixes the way we pick the color to crop out when
using older layout libraries (without transparency support). We now
pick a pixel closer to the bounds of the rendered view such that we
for example can handle themes like the dialog theme.
Change-Id: Ifcbb840ef715bf26a9609f6996a4f136de5ca754
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com')
4 files changed, 388 insertions, 163 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageOverlay.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageOverlay.java index 37029a1..337e76e 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageOverlay.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageOverlay.java @@ -84,7 +84,7 @@ public class ImageOverlay extends Overlay { mImage = null; } else { - mImage = SwtUtils.convertImage(mCanvas.getDisplay(), awtImage, false, -1); + mImage = SwtUtils.convertToSwt(mCanvas.getDisplay(), awtImage, false, -1); } return mImage; 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 new file mode 100644 index 0000000..21ad07c --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageUtils.java @@ -0,0 +1,326 @@ +/* + * Copyright (C) 2010 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.ide.common.api.Rect; + +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; + +/** + * Utilities related to image processing. + */ +public class ImageUtils { + /** + * Returns true if the given image has no dark pixels + * + * @param image the image to be checked for dark pixels + * @return true if no dark pixels were found + */ + public static boolean containsDarkPixels(BufferedImage image) { + for (int y = 0, height = image.getHeight(); y < height; y++) { + for (int x = 0, width = image.getWidth(); x < width; x++) { + int pixel = image.getRGB(x, y); + if ((pixel & 0xFF000000) != 0) { + int r = (pixel & 0xFF0000) >> 16; + int g = (pixel & 0x00FF00) >> 8; + int b = (pixel & 0x0000FF); + + // One perceived luminance formula is (0.299*red + 0.587*green + 0.114*blue) + // In order to keep this fast since we don't need a very accurate + // measure, I'll just estimate this with integer math: + long brightness = (299L*r + 587*g + 114*b) / 1000; + if (brightness < 128) { + return true; + } + } + } + } + return false; + } + + + /** + * Crops blank pixels from the edges of the image and returns the cropped result. We + * crop off pixels that are blank (meaning they have an alpha value = 0). Note that + * this is not the same as pixels that aren't opaque (an alpha value other than 255). + * + * @param image the image to be cropped + * @param initialCrop If not null, specifies a rectangle which contains an initial + * crop to continue. This can be used to crop an image where you already + * know about margins in the image + * @return a cropped version of the source image, or null if the whole image was blank + * and cropping completely removed everything + */ + public static BufferedImage cropBlank(BufferedImage image, Rect initialCrop) { + CropFilter filter = new CropFilter() { + public boolean crop(BufferedImage bufferedImage, int x, int y) { + int rgb = bufferedImage.getRGB(x, y); + return (rgb & 0xFF000000) == 0x00000000; + // TODO: Do a threshold of 80 instead of just 0? Might give better + // visual results -- e.g. check <= 0x80000000 + } + }; + return crop(image, filter, initialCrop); + } + + /** + * Crops pixels of a given color from the edges of the image and returns the cropped + * result. + * + * @param image the image to be cropped + * @param blankArgb the color considered to be blank, as a 32 pixel integer with 8 + * bits of alpha, red, green and blue + * @param initialCrop If not null, specifies a rectangle which contains an initial + * crop to continue. This can be used to crop an image where you already + * know about margins in the image + * @return a cropped version of the source image, or null if the whole image was blank + * and cropping completely removed everything + */ + public static BufferedImage cropColor(BufferedImage image, + final int blankArgb, Rect initialCrop) { + CropFilter filter = new CropFilter() { + public boolean crop(BufferedImage bufferedImage, int x, int y) { + return blankArgb == bufferedImage.getRGB(x, y); + } + }; + return crop(image, filter, initialCrop); + } + + /** + * Interface implemented by cropping functions that determine whether + * a pixel should be cropped or not. + */ + private static interface CropFilter { + /** + * Returns true if the pixel is should be cropped. + * + * @param image the image containing the pixel in question + * @param x the x position of the pixel + * @param y the y position of the pixel + * @return true if the pixel should be cropped (for example, is blank) + */ + boolean crop(BufferedImage image, int x, int y); + } + + private static BufferedImage crop(BufferedImage image, CropFilter filter, Rect initialCrop) { + if (image == null) { + return null; + } + + // First, determine the dimensions of the real image within the image + int x1, y1, x2, y2; + if (initialCrop != null) { + x1 = initialCrop.x; + y1 = initialCrop.y; + x2 = initialCrop.x + initialCrop.w; + y2 = initialCrop.y + initialCrop.h; + } else { + x1 = 0; + y1 = 0; + x2 = image.getWidth(); + y2 = image.getHeight(); + } + + // Nothing left to crop + if (x1 == x2 || y1 == y2) { + return null; + } + + // This algorithm is a bit dumb -- it just scans along the edges looking for + // a pixel that shouldn't be cropped. I could maybe try to make it smarter by + // for example doing a binary search to quickly eliminate large empty areas to + // the right and bottom -- but this is slightly tricky with components like the + // AnalogClock where I could accidentally end up finding a blank horizontal or + // vertical line somewhere in the middle of the rendering of the clock, so for now + // we do the dumb thing -- not a big deal since we tend to crop reasonably + // small images. + + // First determine top edge + topEdge: for (; y1 < y2; y1++) { + for (int x = x1; x < x2; x++) { + if (!filter.crop(image, x, y1)) { + break topEdge; + } + } + } + + if (y1 == image.getHeight()) { + // The image is blank + return null; + } + + // Next determine left edge + leftEdge: for (; x1 < x2; x1++) { + for (int y = y1; y < y2; y++) { + if (!filter.crop(image, x1, y)) { + break leftEdge; + } + } + } + + // Next determine right edge + rightEdge: for (; x2 > x1; x2--) { + for (int y = y1; y < y2; y++) { + if (!filter.crop(image, x2 - 1, y)) { + break rightEdge; + } + } + } + + // Finally determine bottom edge + bottomEdge: for (; y2 > y1; y2--) { + for (int x = x1; x < x2; x++) { + if (!filter.crop(image, x, y2 - 1)) { + break bottomEdge; + } + } + } + + // No need to crop? + if (x1 == 0 && y1 == 0 && x2 == image.getWidth() && y2 == image.getHeight()) { + return image; + } + + if (x1 == x2 || y1 == y2) { + // Nothing left after crop -- blank image + return null; + } + + int width = x2 - x1; + int height = y2 - y1; + + // Now extract the sub-image + BufferedImage cropped = new BufferedImage(width, height, image.getType()); + Graphics g = cropped.getGraphics(); + g.drawImage(image, 0, 0, width, height, x1, y1, x2, y2, null); + + g.dispose(); + + return cropped; + } + + /** + * Creates a drop shadow of a given image and returns a new image which shows the + * input image on top of its drop shadow. + * + * @param source the source image to be shadowed + * @param shadowSize the size of the shadow in pixels + * @param shadowOpacity the opacity of the shadow, with 0=transparent and 1=opaque + * @param shadowRgb the RGB int to use for the shadow color + * @return a new image with the source image on top of its shadow + */ + public static BufferedImage createDropShadow(BufferedImage source, int shadowSize, + float shadowOpacity, int shadowRgb) { + + // This code is based on + // http://www.jroller.com/gfx/entry/non_rectangular_shadow + + BufferedImage image = new BufferedImage(source.getWidth() + shadowSize * 2, + source.getHeight() + shadowSize * 2, + BufferedImage.TYPE_INT_ARGB); + + Graphics2D g2 = image.createGraphics(); + g2.drawImage(source, null, shadowSize, shadowSize); + + int dstWidth = image.getWidth(); + int dstHeight = image.getHeight(); + + int left = (shadowSize - 1) >> 1; + int right = shadowSize - left; + int xStart = left; + int xStop = dstWidth - right; + int yStart = left; + int yStop = dstHeight - right; + + shadowRgb = shadowRgb & 0x00FFFFFF; + + int[] aHistory = new int[shadowSize]; + int historyIdx = 0; + + int aSum; + + int[] dataBuffer = ((DataBufferInt) image.getRaster().getDataBuffer()).getData(); + int lastPixelOffset = right * dstWidth; + float sumDivider = shadowOpacity / shadowSize; + + // horizontal pass + for (int y = 0, bufferOffset = 0; y < dstHeight; y++, bufferOffset = y * dstWidth) { + aSum = 0; + historyIdx = 0; + for (int x = 0; x < shadowSize; x++, bufferOffset++) { + int a = dataBuffer[bufferOffset] >>> 24; + aHistory[x] = a; + aSum += a; + } + + bufferOffset -= right; + + for (int x = xStart; x < xStop; x++, bufferOffset++) { + int a = (int) (aSum * sumDivider); + dataBuffer[bufferOffset] = a << 24 | shadowRgb; + + // subtract the oldest pixel from the sum + aSum -= aHistory[historyIdx]; + + // get the latest pixel + a = dataBuffer[bufferOffset + right] >>> 24; + aHistory[historyIdx] = a; + aSum += a; + + if (++historyIdx >= shadowSize) { + historyIdx -= shadowSize; + } + } + } + // vertical pass + for (int x = 0, bufferOffset = 0; x < dstWidth; x++, bufferOffset = x) { + aSum = 0; + historyIdx = 0; + for (int y = 0; y < shadowSize; y++, bufferOffset += dstWidth) { + int a = dataBuffer[bufferOffset] >>> 24; + aHistory[y] = a; + aSum += a; + } + + bufferOffset -= lastPixelOffset; + + for (int y = yStart; y < yStop; y++, bufferOffset += dstWidth) { + int a = (int) (aSum * sumDivider); + dataBuffer[bufferOffset] = a << 24 | shadowRgb; + + // subtract the oldest pixel from the sum + aSum -= aHistory[historyIdx]; + + // get the latest pixel + a = dataBuffer[bufferOffset + lastPixelOffset] >>> 24; + aHistory[historyIdx] = a; + aSum += a; + + if (++historyIdx >= shadowSize) { + historyIdx -= shadowSize; + } + } + } + + g2.drawImage(source, null, 0, 0); + g2.dispose(); + + return image; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PaletteComposite.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PaletteComposite.java index 32bf15b..6cfaff6 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PaletteComposite.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PaletteComposite.java @@ -555,7 +555,7 @@ public class PaletteComposite extends Composite { private static final int RENDER_WIDTH = 500; /** Amount of alpha to multiply into the image (divided by 256) */ - private static final int IMG_ALPHA = 192; + private static final int IMG_ALPHA = 216; /** The item this preview is rendering a preview for */ private final Item mItem; @@ -590,14 +590,22 @@ public class PaletteComposite extends Composite { final Image image = new Image(mItem.getDisplay(), size.x, size.y); gc.copyArea(image, 0, 0); gc.dispose(); - ImageData data = image.getImageData(); - data.alpha = IMG_ALPHA; + Display display = mItem.getDisplay(); + BufferedImage awtImage = SwtUtils.convertToAwt(image); + if (awtImage != null) { + awtImage = ImageUtils.createDropShadow(awtImage, 3 /* shadowSize */, + 0.7f /* shadowAlpha */, 0x000000 /* shadowRgb */); + mImage = SwtUtils.convertToSwt(display, awtImage, true, IMG_ALPHA); + } else { + ImageData data = image.getImageData(); + data.alpha = IMG_ALPHA; - // Changing the ImageData -after- constructing an image on it - // has no effect, so we have to construct a new image. Luckily these - // are tiny images. - mImage = new Image(mItem.getDisplay(), data); + // Changing the ImageData -after- constructing an image on it + // has no effect, so we have to construct a new image. Luckily these + // are tiny images. + mImage = new Image(display, data); + } image.dispose(); } @@ -707,15 +715,39 @@ public class PaletteComposite extends Composite { } if (hasTransparency) { - cropped = SwtUtils.cropBlank(image, initialCrop); + cropped = ImageUtils.cropBlank(image, initialCrop); } else { - int edgeColor = image.getRGB(image.getWidth() - 1, image.getHeight() - 1); - cropped = SwtUtils.cropColor(image, edgeColor, initialCrop); + // Find out what the "background" color is such that we can properly + // crop it out of the image. To do this we pick out a pixel in the + // bottom right unpainted area. Rather than pick the one in the far + // bottom corner, we pick one as close to the bounds of the view as + // possible (but still outside of the bounds), such that we can + // deal with themes like the dialog theme. + int edgeX = image.getWidth() -1; + int edgeY = image.getHeight() -1; + if (viewInfo != null) { + if (viewInfo.getRight() < image.getWidth()-1) { + edgeX = viewInfo.getRight()+1; + } + if (viewInfo.getBottom() < image.getHeight()-1) { + edgeY = viewInfo.getBottom()+1; + } + } + int edgeColor = image.getRGB(edgeX, edgeY); + cropped = ImageUtils.cropColor(image, edgeColor, initialCrop); } if (cropped != null) { + boolean needsContrast = hasTransparency + && !ImageUtils.containsDarkPixels(cropped); + cropped = ImageUtils.createDropShadow(cropped, + hasTransparency ? 3 : 5 /* shadowSize */, + !hasTransparency ? 0.6f : needsContrast ? 0.8f : 0.7f /* alpha */, + 0x000000 /* shadowRgb */); + Display display = getControl().getDisplay(); - Image swtImage = SwtUtils.convertImage(display, cropped, true, IMG_ALPHA); + int alpha = (!hasTransparency || !needsContrast) ? IMG_ALPHA : -1; + Image swtImage = SwtUtils.convertToSwt(display, cropped, true, alpha); return swtImage; } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SwtUtils.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SwtUtils.java index 3080716..6c55dd3 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SwtUtils.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SwtUtils.java @@ -15,20 +15,17 @@ */ package com.android.ide.eclipse.adt.internal.editors.layout.gle2; -import com.android.ide.common.api.Rect; - import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.ImageData; import org.eclipse.swt.graphics.PaletteData; import org.eclipse.swt.widgets.Display; -import java.awt.Graphics; import java.awt.image.BufferedImage; import java.awt.image.DataBufferInt; import java.awt.image.Raster; /** - * Various generic SWT utilities such as image conversion and cropping. + * Various generic SWT utilities such as image conversion. */ public class SwtUtils { private SwtUtils() { @@ -47,7 +44,7 @@ public class SwtUtils { * @return A new SWT {@link Image} with the same contents as the source * {@link BufferedImage} */ - public static Image convertImage(Display display, BufferedImage awtImage, + public static Image convertToSwt(Display display, BufferedImage awtImage, boolean transferAlpha, int globalAlpha) { int width = awtImage.getWidth(); int height = awtImage.getHeight(); @@ -88,158 +85,28 @@ public class SwtUtils { } /** - * Crops blank pixels from the edges of the image and returns the cropped result. We - * crop off pixels that are blank (meaning they have an alpha value = 0). Note that - * this is not the same as pixels that aren't opaque (an alpha value other than 255). - * - * @param image the image to be cropped - * @param initialCrop If not null, specifies a rectangle which contains an initial - * crop to continue. This can be used to crop an image where you already - * know about margins in the image - * @return a cropped version of the source image, or null if the whole image was blank - * and cropping completely removed everything - */ - public static BufferedImage cropBlank(BufferedImage image, Rect initialCrop) { - CropFilter filter = new CropFilter() { - public boolean crop(BufferedImage bufferedImage, int x, int y) { - int rgb = bufferedImage.getRGB(x, y); - return (rgb & 0xFF000000) == 0x00000000; - // TODO: Do a threshold of 80 instead of just 0? Might give better - // visual results -- e.g. check <= 0x80000000 - } - }; - return crop(image, filter, initialCrop); - } - - /** - * Crops pixels of a given color from the edges of the image and returns the cropped - * result. + * Converts a direct-color model SWT image to an equivalent AWT image. If the image + * does not have a supported color model, returns null. This method does <b>NOT</b> + * preserve alpha in the source image. * - * @param image the image to be cropped - * @param blankRgba the color considered to be blank, as a 32 pixel integer with 8 - * bits of alpha, red, green and blue - * @param initialCrop If not null, specifies a rectangle which contains an initial - * crop to continue. This can be used to crop an image where you already - * know about margins in the image - * @return a cropped version of the source image, or null if the whole image was blank - * and cropping completely removed everything - */ - public static BufferedImage cropColor(BufferedImage image, - final int blankRgba, Rect initialCrop) { - CropFilter filter = new CropFilter() { - public boolean crop(BufferedImage bufferedImage, int x, int y) { - return blankRgba == bufferedImage.getRGB(x, y); - } - }; - return crop(image, filter, initialCrop); - } - - /** - * Interface implemented by cropping functions that determine whether - * a pixel should be cropped or not. + * @param swtImage the SWT image to be converted to AWT + * @return an AWT image representing the source SWT image */ - private static interface CropFilter { - /** - * Returns true if the pixel is should be cropped. - * - * @param image the image containing the pixel in question - * @param x the x position of the pixel - * @param y the y position of the pixel - * @return true if the pixel should be cropped (for example, is blank) - */ - boolean crop(BufferedImage image, int x, int y); - } - - private static BufferedImage crop(BufferedImage image, CropFilter filter, Rect initialCrop) { - if (image == null) { - return null; - } - - // First, determine the dimensions of the real image within the image - int x1, y1, x2, y2; - if (initialCrop != null) { - x1 = initialCrop.x; - y1 = initialCrop.y; - x2 = initialCrop.x + initialCrop.w; - y2 = initialCrop.y + initialCrop.h; - } else { - x1 = 0; - y1 = 0; - x2 = image.getWidth(); - y2 = image.getHeight(); - } - - // Nothing left to crop - if (x1 == x2 || y1 == y2) { - return null; - } - - // This algorithm is a bit dumb -- it just scans along the edges looking for - // a pixel that shouldn't be cropped. I could maybe try to make it smarter by - // for example doing a binary search to quickly eliminate large empty areas to - // the right and bottom -- but this is slightly tricky with components like the - // AnalogClock where I could accidentally end up finding a blank horizontal or - // vertical line somewhere in the middle of the rendering of the clock, so for now - // we do the dumb thing -- not a big deal since we tend to crop reasonably - // small images. - - // First determine top edge - topEdge: for (; y1 < y2; y1++) { - for (int x = x1; x < x2; x++) { - if (!filter.crop(image, x, y1)) { - break topEdge; - } - } - } - - if (y1 == image.getHeight()) { - // The image is blank - return null; - } - - // Next determine left edge - leftEdge: for (; x1 < x2; x1++) { - for (int y = y1; y < y2; y++) { - if (!filter.crop(image, x1, y)) { - break leftEdge; - } - } - } - - // Next determine right edge - rightEdge: for (; x2 > x1; x2--) { - for (int y = y1; y < y2; y++) { - if (!filter.crop(image, x2 - 1, y)) { - break rightEdge; - } - } - } - - // Finally determine bottom edge - bottomEdge: for (; y2 > y1; y2--) { - for (int x = x1; x < x2; x++) { - if (!filter.crop(image, x, y2 - 1)) { - break bottomEdge; + public static BufferedImage convertToAwt(Image swtImage) { + ImageData data = swtImage.getImageData(); + BufferedImage awtImage = new BufferedImage(data.width, data.height, BufferedImage.TYPE_INT_ARGB); + PaletteData palette = data.palette; + if (palette.isDirect) { + for (int y = 0; y < data.height; y++) { + for (int x = 0; x < data.width; x++) { + int pixel = data.getPixel(x, y); + awtImage.setRGB(x, y, 0xFF000000 | pixel); } } - } - - // No need to crop? - if (x1 == 0 && y1 == 0 && x2 == image.getWidth() && y2 == image.getHeight()) { - return image; - } - - if (x1 == x2 || y1 == y2) { - // Nothing left after crop -- blank image + } else { return null; } - // Now extract the sub-image - BufferedImage cropped = new BufferedImage(x2 - x1, y2 - y1, image.getType()); - Graphics g = cropped.getGraphics(); - g.drawImage(image, 0, 0, x2 - x1, y2 - y1, x1, y1, x2, y2, null); - g.dispose(); - - return cropped; + return awtImage; } } |