| /* |
| * Copyright (C) 2013 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.android.tools.idea.rendering; |
| |
| import com.android.ide.common.rendering.HardwareConfigHelper; |
| import com.android.resources.ScreenOrientation; |
| import com.android.sdklib.devices.Device; |
| import com.android.sdklib.devices.State; |
| import com.android.tools.idea.configurations.Configuration; |
| import com.android.tools.idea.ddms.screenshot.DeviceArtPainter; |
| import com.intellij.openapi.util.SystemInfo; |
| import com.intellij.util.ui.UIUtil; |
| import org.jetbrains.android.uipreview.AndroidEditorSettings; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import java.awt.*; |
| import java.awt.geom.Ellipse2D; |
| import java.awt.image.BufferedImage; |
| |
| import static com.android.tools.idea.rendering.ShadowPainter.SHADOW_SIZE; |
| import static com.android.tools.idea.rendering.ShadowPainter.SMALL_SHADOW_SIZE; |
| import static java.awt.RenderingHints.KEY_INTERPOLATION; |
| import static java.awt.RenderingHints.VALUE_INTERPOLATION_BILINEAR; |
| |
| /** |
| * A rendered image from layoutlib, which can be zoomed, can have decorations |
| * such as drop shadows or even device chrome, and so on. |
| */ |
| public class RenderedImage { |
| /** Type of shadow to paint into the image, if any */ |
| enum ShadowType { |
| /** Don't draw a drop shadow */ |
| NONE, |
| /** Draw a rectangular shadow. This is faster than {@link #ARBITRARY} */ |
| RECTANGULAR, |
| /** Draw an arbitrarily shaped drop shadow */ |
| ARBITRARY |
| } |
| |
| @NotNull private final BufferedImage myImage; |
| @Nullable private BufferedImage myScaledImage; |
| @NotNull private Configuration myConfiguration; |
| @Nullable private Rectangle myImageBounds; |
| private final boolean myAlphaChannelImage; |
| private final ShadowType myShadowType; |
| private double myScale = 1; |
| private int myMaxWidth; |
| private int myMaxHeight; |
| private boolean myUseLargeShadows = true; |
| private boolean myDeviceFrameEnabled = true; |
| /** Whether current thumbnail actually has a device frame */ |
| private boolean myThumbnailHasFrame; |
| |
| public RenderedImage(@NotNull Configuration configuration, |
| @NotNull BufferedImage image, |
| boolean alphaChannelImage, |
| @NotNull ShadowType shadowType) { |
| myConfiguration = configuration; |
| myImage = image; |
| myAlphaChannelImage = alphaChannelImage; |
| myShadowType = shadowType; |
| } |
| |
| public boolean hasAlphaChannel() { |
| return myAlphaChannelImage; |
| } |
| |
| /** |
| * Returns whether this image overlay should be painted with a drop shadow. |
| * This is usually the case, but not for transparent themes like the dialog |
| * theme (Theme.*Dialog), which already provides its own shadow. |
| * |
| * @return true if the image overlay should be shown with a drop shadow. |
| */ |
| public boolean hasDropShadow() { |
| return myShadowType != ShadowType.NONE; |
| } |
| |
| public double getScale() { |
| return myScale; |
| } |
| |
| public void setScale(double scale) { |
| if (myMaxWidth > 0) { |
| // If we have a fixed size, ignore scale factor |
| assert myMaxHeight > 0; |
| double imageWidth = myImage.getWidth(); |
| double imageHeight = myImage.getHeight(); |
| scale = Math.min(1, Math.min(myMaxWidth / imageWidth, myMaxHeight / imageHeight)); |
| } |
| |
| if (myScale != scale) { |
| myScaledImage = null; |
| myScale = scale; |
| |
| // Normalize the scale: |
| // Some operations are faster if the zoom is EXACTLY 1.0 rather than ALsMOST 1.0. |
| // (This is because there is a fast-path when image copying and the scale is 1.0; |
| // in that case it does not have to do any scaling). |
| // |
| // If you zoom out 10 times and then back in 10 times, small rounding errors mean |
| // that you end up with a scale=1.0000000000000004. In the cases, when you get close |
| // to 1.0, just make the zoom an exact 1.0. |
| if (Math.abs(myScale - 1.0) < 0.01) { |
| myScale = 1.0; |
| } |
| } |
| } |
| |
| /** |
| * Returns the bounds of the image itself, if it is surrounded by a device frame. |
| * Null otherwise. |
| * |
| * @return the image bounds, or null |
| */ |
| @Nullable |
| public Rectangle getImageBounds() { |
| if (myImageBounds == null && myScaledImage == null) { |
| // Haven't painted and computed the image bounds yet, but need computation such that we can |
| if (myScale <= 1 && myDeviceFrameEnabled) { |
| DeviceArtPainter framePainter = DeviceArtPainter.getInstance(); |
| Device device = myConfiguration.getDevice(); |
| if (device != null) { |
| AndroidEditorSettings.GlobalState settings = AndroidEditorSettings.getInstance().getGlobalState(); |
| if (settings.isShowDeviceFrames()) { |
| State deviceState = myConfiguration.getDeviceState(); |
| if (deviceState != null) { |
| double scale = myScale; |
| ScreenOrientation orientation = deviceState.getOrientation(); |
| double frameFactor = framePainter.getFrameMaxOverhead(device, orientation); |
| scale /= frameFactor; |
| myImageBounds = framePainter.computeBounds(myImage.getWidth(), myImage.getHeight(), device, orientation, scale); |
| myThumbnailHasFrame = true; |
| } |
| } |
| } |
| } |
| } |
| return myImageBounds; |
| } |
| |
| /** |
| * Zooms the view to fit. |
| * |
| * @param availableWidth the available view width |
| * @param availableHeight the available view height |
| * @param allowZoomIn if true, apply the scale such that it always fills the available space; if |
| * false, allow zoom out, but never zoom in more than 100% (the real size) |
| * @param horizontalMargin optional horizontal margin to reserve room for |
| * @param verticalMargin optional vertical margin to reserve room for |
| */ |
| public void zoomToFit(int availableWidth, int availableHeight, boolean allowZoomIn, int horizontalMargin, int verticalMargin) { |
| int sceneWidth = myImage.getWidth(); |
| int sceneHeight = myImage.getHeight(); |
| |
| int shadowSize = hasDropShadow() ? myUseLargeShadows ? SHADOW_SIZE : SMALL_SHADOW_SIZE : 0; |
| availableWidth -= shadowSize; |
| availableHeight -= shadowSize; |
| |
| if (sceneWidth > 0 && sceneHeight > 0) { |
| // Reduce the margins if necessary |
| int hDelta = availableWidth - sceneWidth; |
| int xMargin = 0; |
| if (hDelta > 2 * horizontalMargin) { |
| xMargin = horizontalMargin; |
| } else if (hDelta > 0) { |
| xMargin = hDelta / 2; |
| } |
| |
| int vDelta = availableHeight - sceneHeight; |
| int yMargin = 0; |
| if (vDelta > 2 * verticalMargin) { |
| yMargin = verticalMargin; |
| } else if (vDelta > 0) { |
| yMargin = vDelta / 2; |
| } |
| |
| double hScale = (availableWidth - 2 * xMargin) / (double) sceneWidth; |
| double vScale = (availableHeight - 2 * yMargin) / (double) sceneHeight; |
| |
| double scale = Math.min(hScale, vScale); |
| |
| if (!allowZoomIn) { |
| scale = Math.min(1.0, scale); |
| } |
| |
| setScale(scale); |
| } |
| } |
| |
| private static final double ZOOM_FACTOR = 1.2; |
| |
| public void zoomIn() { |
| setScale(myScale * ZOOM_FACTOR); |
| } |
| |
| public void zoomOut() { |
| setScale(myScale / ZOOM_FACTOR); |
| } |
| |
| public void zoomActual() { |
| if (SystemInfo.isMac && UIUtil.isRetina()) { |
| setScale(0.5); |
| } else { |
| setScale(1); |
| } |
| } |
| |
| /** Returns the original full size rendered image */ |
| @NotNull |
| public BufferedImage getOriginalImage() { |
| return myImage; |
| } |
| |
| /** Returns the original width of the image itself, not scaled */ |
| public int getOriginalWidth() { |
| return myImage.getWidth(); |
| } |
| |
| /** Returns the original height of the image itself, not scaled */ |
| public int getOriginalHeight() { |
| return myImage.getHeight(); |
| } |
| |
| /** Returns the width of the image itself, when scaled */ |
| public int getScaledWidth() { |
| return (int)(myScale * myImage.getWidth()); |
| } |
| |
| /** Returns the height of the image itself, when scaled */ |
| public int getScaledHeight() { |
| return (int)(myScale * myImage.getHeight()); |
| } |
| |
| public Dimension getScaledSize() { |
| return new Dimension(getScaledWidth(), getScaledHeight()); |
| } |
| |
| /** Returns the required width to show the scaled image, including drop shadows if applicable */ |
| public int getRequiredWidth() { |
| int width = (int)(myScale * myImage.getWidth()); |
| if (myThumbnailHasFrame || myImageBounds == null && myScaledImage == null && myDeviceFrameEnabled && |
| AndroidEditorSettings.getInstance().getGlobalState().isShowDeviceFrames()) { |
| DeviceArtPainter framePainter = DeviceArtPainter.getInstance(); |
| Device device = myConfiguration.getDevice(); |
| if (device != null) { |
| State deviceState = myConfiguration.getDeviceState(); |
| if (deviceState != null) { |
| double frameFactor = framePainter.getFrameWidthOverhead(device, deviceState.getOrientation()); |
| width *= frameFactor; |
| return width; |
| } |
| } |
| } |
| |
| if (hasDropShadow()) { |
| width += myUseLargeShadows ? SHADOW_SIZE : SMALL_SHADOW_SIZE; |
| } |
| |
| return width; |
| } |
| |
| /** Returns the required height to show the scaled image, including drop shadows if applicable */ |
| public int getRequiredHeight() { |
| int height = (int)(myScale * myImage.getHeight()); |
| if (myThumbnailHasFrame || myImageBounds == null && myScaledImage == null && myDeviceFrameEnabled && |
| AndroidEditorSettings.getInstance().getGlobalState().isShowDeviceFrames()) { |
| DeviceArtPainter framePainter = DeviceArtPainter.getInstance(); |
| Device device = myConfiguration.getDevice(); |
| if (device != null) { |
| State deviceState = myConfiguration.getDeviceState(); |
| if (deviceState != null) { |
| double frameFactor = framePainter.getFrameHeightOverhead(device, deviceState.getOrientation()); |
| height *= frameFactor; |
| return height; |
| } |
| } |
| } |
| |
| if (hasDropShadow()) { |
| height += myUseLargeShadows ? SHADOW_SIZE : SMALL_SHADOW_SIZE; |
| } |
| |
| return height; |
| } |
| |
| /** Returns the required size to show the scaled image, including drop shadows if applicable */ |
| public Dimension getRequiredSize() { |
| return new Dimension(getRequiredWidth(), getRequiredHeight()); |
| } |
| |
| public void paint(@NotNull Graphics g, int x, int y) { |
| if (UIUtil.isRetina() && paintRetina(g, x, y)) { |
| return; |
| } |
| |
| if (myScaledImage == null) { |
| // Special cases myScale=1 to be fast |
| myImageBounds = null; |
| myThumbnailHasFrame = false; |
| |
| if (myScale <= 1 && myDeviceFrameEnabled) { |
| DeviceArtPainter framePainter = DeviceArtPainter.getInstance(); |
| Device device = myConfiguration.getDevice(); |
| if (device != null) { |
| AndroidEditorSettings.GlobalState settings = AndroidEditorSettings.getInstance().getGlobalState(); |
| if (settings.isShowDeviceFrames()) { |
| boolean showEffects = settings.isShowEffects(); |
| State deviceState = myConfiguration.getDeviceState(); |
| if (deviceState != null) { |
| double scale = myScale; |
| ScreenOrientation orientation = deviceState.getOrientation(); |
| double frameFactor = framePainter.getFrameMaxOverhead(device, orientation); |
| scale /= frameFactor; |
| myImageBounds = new Rectangle(); |
| myScaledImage = framePainter.createFrame(myImage, device, orientation, showEffects, scale, myImageBounds); |
| myThumbnailHasFrame = true; |
| paint(g, x, y); |
| return; |
| } |
| } |
| } |
| } |
| |
| if (myScale == 1) { |
| // Scaling to 100% is easy! |
| myScaledImage = myImage; |
| // ...unless we need to clip: |
| Device device = myConfiguration.getDevice(); |
| if (device != null && device.isScreenRound()) { |
| int imageType = myScaledImage.getType(); |
| if (imageType == BufferedImage.TYPE_CUSTOM) { |
| imageType = BufferedImage.TYPE_INT_ARGB; |
| } |
| @SuppressWarnings("UndesirableClassUsage") // layoutlib doesn't create retina images |
| BufferedImage clipped = new BufferedImage(myImage.getWidth(), myImage.getHeight(), imageType); |
| Graphics2D g2 = clipped.createGraphics(); |
| g2.setComposite(AlphaComposite.Src); |
| //noinspection UseJBColor |
| g2.setColor(new Color(0, true)); |
| g2.fillRect(0, 0, clipped.getWidth(), clipped.getHeight()); |
| paintClipped(g2, myImage, device, 0, 0, true); |
| g2.dispose(); |
| myScaledImage = clipped; |
| } |
| |
| if (myShadowType == ShadowType.RECTANGULAR) { |
| // Just need to draw drop shadows |
| if (myUseLargeShadows) { |
| myScaledImage = ShadowPainter.createRectangularDropShadow(myScaledImage); |
| } else { |
| myScaledImage = ShadowPainter.createSmallRectangularDropShadow(myScaledImage); |
| } |
| } else if (myShadowType == ShadowType.ARBITRARY) { |
| int shadowSize = myUseLargeShadows ? SHADOW_SIZE : SMALL_SHADOW_SIZE; |
| myScaledImage = ShadowPainter.createDropShadow(myScaledImage, shadowSize); |
| } |
| //noinspection ConstantConditions |
| UIUtil.drawImage(g, myScaledImage, x, y, null); |
| } else if (myScale < 1) { |
| // When scaling down we need to do an expensive scaling to ensure that |
| // the thumbnails look good |
| if (myShadowType != ShadowType.NONE) { |
| int shadowSize = myUseLargeShadows ? SHADOW_SIZE : SMALL_SHADOW_SIZE; |
| if (myShadowType == ShadowType.ARBITRARY) { |
| int scaledWidth = (int)(myImage.getWidth() * myScale); |
| int scaledHeight = (int)(myImage.getHeight() * myScale); |
| Shape clip = getClip(myConfiguration.getDevice(), 0, 0, scaledWidth, scaledHeight); |
| myScaledImage = ImageUtils.scale(myImage, myScale, myScale, clip); |
| myScaledImage = ShadowPainter.createDropShadow(myScaledImage, shadowSize); |
| } else { |
| // Clip should not apply here |
| // Reserve room for the shadow; then paint directly into the target image |
| myScaledImage = ImageUtils.scale(myImage, myScale, myScale, shadowSize, shadowSize); |
| if (myUseLargeShadows) { |
| ShadowPainter.drawRectangleShadow(myScaledImage, 0, 0, myScaledImage.getWidth() - shadowSize, |
| myScaledImage.getHeight() - shadowSize); |
| } else { |
| ShadowPainter.drawSmallRectangleShadow(myScaledImage, 0, 0, myScaledImage.getWidth() - shadowSize, |
| myScaledImage.getHeight() - shadowSize); |
| } |
| } |
| } else { |
| // Clip should not apply here |
| myScaledImage = ImageUtils.scale(myImage, myScale, myScale); |
| } |
| //noinspection ConstantConditions |
| UIUtil.drawImage(g, myScaledImage, x, y, null); |
| } else { |
| // Do a direct scaled paint when scaling up; we don't want to create giant internal images |
| // for a zoomed in version of the canvas, since only a small portion is typically shown on the screen |
| // (without this, you can easily zoom in 10 times and hit an OOM exception) |
| double scale = myScale; |
| int w = myImage.getWidth(); |
| int h = myImage.getHeight(); |
| int scaledWidth = (int)(scale * w); |
| int scaledHeight = (int)(scale * h); |
| myThumbnailHasFrame = false; |
| |
| Device device = myConfiguration.getDevice(); |
| boolean round = device != null && device.isScreenRound(); |
| |
| Graphics2D g2 = (Graphics2D)g.create(); |
| try { |
| g2.setRenderingHint(KEY_INTERPOLATION, VALUE_INTERPOLATION_BILINEAR); |
| |
| Shape prevClip = null; |
| if (round) { |
| prevClip = g.getClip(); |
| int extra = 0; |
| g2.setClip(new Ellipse2D.Double(x - extra, y - extra, scaledWidth + 2 * extra, scaledHeight + 2 * extra)); |
| } |
| |
| g2.drawImage(myImage, x, y, x + scaledWidth, y + scaledHeight, 0, 0, w, h, null); |
| |
| if (round) { |
| g.setClip(prevClip); |
| } |
| } finally { |
| g2.dispose(); |
| } |
| |
| myThumbnailHasFrame = false; |
| if (myDeviceFrameEnabled) { |
| DeviceArtPainter framePainter = DeviceArtPainter.getInstance(); |
| if (device != null) { |
| AndroidEditorSettings.GlobalState settings = AndroidEditorSettings.getInstance().getGlobalState(); |
| if (settings.isShowDeviceFrames()) { |
| State state = myConfiguration.getDeviceState(); |
| if (state != null) { |
| // Scaling larger than 1 |
| boolean showEffects = settings.isShowEffects(); |
| framePainter.paintFrame(g, device, state.getOrientation(), showEffects, x + 1, y, scaledHeight); |
| myThumbnailHasFrame = true; |
| return; |
| } // else: fall through and do usual drop shadow painting |
| } |
| } |
| } |
| |
| if (hasDropShadow() && myShadowType == ShadowType.RECTANGULAR) { |
| // We don't draw arbitrary drop shadows in up-scale mode; hopefully the visual artifacts aren't too noticeable |
| ShadowPainter.drawRectangleShadow(g, x, y, scaledWidth, scaledHeight); |
| } |
| } |
| } else { |
| //noinspection ConstantConditions |
| UIUtil.drawImage(g, myScaledImage, x, y, null); |
| } |
| } |
| |
| public boolean paintRetina(@NotNull Graphics g, int x, int y) { |
| if (!ImageUtils.supportsRetina()) { |
| return false; |
| } |
| |
| AndroidEditorSettings.GlobalState settings = AndroidEditorSettings.getInstance().getGlobalState(); |
| if (!settings.isRetina()) { |
| return false; |
| } |
| |
| if (myScale > 1.01) { |
| // When scaling up significantly, use normal painting logic; no need to pixel double into a |
| // double res image buffer! |
| return false; |
| } |
| |
| if (myScaledImage == null) { |
| myImageBounds = null; |
| myThumbnailHasFrame = false; |
| |
| BufferedImage image = null; |
| ShadowType shadowType = myShadowType; |
| if (myScale <= 1 && myDeviceFrameEnabled) { |
| DeviceArtPainter framePainter = DeviceArtPainter.getInstance(); |
| Device device = myConfiguration.getDevice(); |
| if (device != null) { |
| if (settings.isShowDeviceFrames()) { |
| boolean showEffects = settings.isShowEffects(); |
| State deviceState = myConfiguration.getDeviceState(); |
| if (deviceState != null) { |
| double scale = myScale; |
| ScreenOrientation orientation = deviceState.getOrientation(); |
| double frameFactor = framePainter.getFrameMaxOverhead(device, orientation); |
| scale /= frameFactor; |
| myImageBounds = new Rectangle(); |
| image = framePainter.createFrame(myImage, device, orientation, showEffects, 2 * scale, myImageBounds); |
| myImageBounds.x /= 2; |
| myImageBounds.y /= 2; |
| myImageBounds.width /= 2; |
| myImageBounds.height /= 2; |
| myThumbnailHasFrame = true; |
| shadowType = ShadowType.NONE; |
| } |
| } |
| } |
| } |
| |
| if (image == null) { |
| image = myImage; |
| |
| Device device = myConfiguration.getDevice(); |
| if (device != null && device.isScreenRound()) { |
| int imageType = image.getType(); |
| if (imageType == BufferedImage.TYPE_CUSTOM) { |
| imageType = BufferedImage.TYPE_INT_ARGB; |
| } |
| @SuppressWarnings("UndesirableClassUsage") // layoutlib doesn't create retina images |
| BufferedImage clipped = new BufferedImage(image.getWidth(), image.getHeight(), imageType); |
| Graphics2D g2 = clipped.createGraphics(); |
| g2.setComposite(AlphaComposite.Src); |
| //noinspection UseJBColor |
| g2.setColor(new Color(0, true)); |
| g2.fillRect(0, 0, clipped.getWidth(), clipped.getHeight()); |
| paintClipped(g2, image, device, 0, 0, true); |
| g2.dispose(); |
| image = clipped; |
| } |
| |
| // No scaling if very close to 1.0 |
| double retinaScale = 2 * myScale; |
| if (Math.abs(myScale - 1.0) > 0.01) { |
| image = ImageUtils.scale(image, retinaScale, retinaScale); |
| } |
| } |
| |
| myScaledImage = ImageUtils.convertToRetina(image); |
| if (myScaledImage == null) { |
| return false; |
| } |
| |
| if (shadowType == ShadowType.RECTANGULAR) { |
| // Just need to draw drop shadows |
| if (myUseLargeShadows) { |
| myScaledImage = ShadowPainter.createRectangularDropShadow(myScaledImage); |
| } |
| else { |
| myScaledImage = ShadowPainter.createSmallRectangularDropShadow(myScaledImage); |
| } |
| } |
| else if (shadowType == ShadowType.ARBITRARY) { |
| int shadowSize = myUseLargeShadows ? SHADOW_SIZE : SMALL_SHADOW_SIZE; |
| myScaledImage = ShadowPainter.createDropShadow(myScaledImage, shadowSize); |
| } |
| } |
| |
| //noinspection ConstantConditions |
| UIUtil.drawImage(g, myScaledImage, x, y, null); |
| return true; |
| } |
| |
| public void setMaxSize(int width, int height) { |
| myMaxWidth = width; |
| myMaxHeight = height; |
| zoomActual(); |
| } |
| |
| public int getMaxWidth() { |
| return myMaxWidth; |
| } |
| |
| public int getMaxHeight() { |
| return myMaxHeight; |
| } |
| |
| public void setUseLargeShadows(boolean useLargeShadows) { |
| myUseLargeShadows = useLargeShadows; |
| } |
| |
| public void setDeviceFrameEnabled(boolean deviceFrameEnabled) { |
| if (myDeviceFrameEnabled != deviceFrameEnabled) { |
| myDeviceFrameEnabled = deviceFrameEnabled; |
| myScaledImage = null; |
| } |
| } |
| |
| /** Does the current image have a device frame around it? Returns true, false, or null if no image computed yet */ |
| @Nullable |
| public Boolean isFramed() { |
| if (myScaledImage == null) { |
| return null; |
| } |
| return myThumbnailHasFrame; |
| } |
| |
| public boolean getShowDropShadow() { |
| return myShadowType != ShadowType.NONE; |
| } |
| |
| public void imageChanged() { |
| myScaledImage = null; |
| myImageBounds = null; |
| } |
| |
| @Nullable |
| public static Shape getClip(@Nullable Device device, int x, int y, int width, int height) { |
| boolean round = device != null && device.isScreenRound(); |
| if (round) { |
| int slop = 3; // to hide mask aliasing effects under device chrome by a pixel or two |
| return new Ellipse2D.Double(x - slop, y - slop, width + 2 * slop, height + 2 * slop); |
| } |
| |
| return null; |
| } |
| |
| |
| /** Paints a rendered device image into the given graphics context */ |
| public static void paintClipped(@NotNull Graphics2D g, |
| @NotNull BufferedImage image, |
| @Nullable Device device, |
| int x, |
| int y, |
| boolean withRetina) { |
| Shape prevClip = null; |
| Shape clip = getClip(device, x, y, image.getWidth(), image.getHeight()); |
| if (clip != null) { |
| prevClip = g.getClip(); |
| g.setClip(clip); |
| } |
| |
| if (withRetina) { |
| //noinspection ConstantConditions |
| UIUtil.drawImage(g, image, x, y, null); |
| } else { |
| g.drawImage(image, x, y, null); |
| } |
| |
| if (clip != null) { |
| g.setClip(prevClip); |
| } |
| } |
| } |