| /* |
| * Copyright (C) 2010 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.layoutlib.bridge.impl; |
| |
| import static java.awt.image.BufferedImage.TYPE_INT_ARGB; |
| import static java.awt.image.BufferedImage.TYPE_INT_RGB; |
| import static java.lang.Math.min; |
| import static java.lang.Math.max; |
| |
| import com.android.ide.common.rendering.api.LayoutLog; |
| import com.android.layoutlib.bridge.Bridge; |
| |
| import android.graphics.Bitmap_Delegate; |
| import android.graphics.Canvas; |
| import android.graphics.ColorFilter_Delegate; |
| import android.graphics.Paint; |
| import android.graphics.Paint_Delegate; |
| import android.graphics.PorterDuff; |
| import android.graphics.PorterDuff.Mode; |
| import android.graphics.Rect; |
| import android.graphics.RectF; |
| import android.graphics.Region; |
| import android.graphics.Region_Delegate; |
| import android.graphics.Shader_Delegate; |
| |
| import java.awt.AlphaComposite; |
| import java.awt.Color; |
| import java.awt.Composite; |
| import java.awt.Graphics2D; |
| import java.awt.Rectangle; |
| import java.awt.RenderingHints; |
| import java.awt.Shape; |
| import java.awt.geom.AffineTransform; |
| import java.awt.geom.Area; |
| import java.awt.geom.NoninvertibleTransformException; |
| import java.awt.geom.Rectangle2D; |
| import java.awt.image.BufferedImage; |
| import java.util.ArrayList; |
| |
| /** |
| * Class representing a graphics context snapshot, as well as a context stack as a linked list. |
| * <p> |
| * This is based on top of {@link Graphics2D} but can operate independently if none are available |
| * yet when setting transforms and clip information. |
| * <p> |
| * This allows for drawing through {@link #draw(Drawable, Paint_Delegate, boolean, boolean)} and |
| * {@link #draw(Drawable)} |
| * |
| * Handling of layers (created with {@link Canvas#saveLayer(RectF, Paint, int)}) is handled through |
| * a list of Graphics2D for each layers. The class actually maintains a list of {@link Layer} |
| * for each layer. Doing a save() will duplicate this list so that each graphics2D object |
| * ({@link Layer#getGraphics()}) is configured only for the new snapshot. |
| */ |
| public class GcSnapshot { |
| private static final AffineTransform IDENTITY_TRANSFORM = new AffineTransform(); |
| |
| private final GcSnapshot mPrevious; |
| private final int mFlags; |
| |
| /** list of layers. The first item in the list is always the */ |
| private final ArrayList<Layer> mLayers = new ArrayList<Layer>(); |
| |
| /** temp transform in case transformation are set before a Graphics2D exists */ |
| private AffineTransform mTransform = null; |
| /** temp clip in case clipping is set before a Graphics2D exists */ |
| private Area mClip = null; |
| |
| // local layer data |
| /** a local layer created with {@link Canvas#saveLayer(RectF, Paint, int)}. |
| * If this is null, this does not mean there's no layer, just that the snapshot is not the |
| * one that created the layer. |
| */ |
| private final Layer mLocalLayer; |
| private final Paint_Delegate mLocalLayerPaint; |
| private final Rect mLayerBounds; |
| |
| public interface Drawable { |
| void draw(Graphics2D graphics, Paint_Delegate paint); |
| } |
| |
| /** |
| * Class containing information about a layer. |
| * |
| * This contains graphics, bitmap and layer information. |
| */ |
| private static class Layer { |
| private final Graphics2D mGraphics; |
| private final Bitmap_Delegate mBitmap; |
| private final BufferedImage mImage; |
| /** the flags that were used to configure the layer. This is never changed, and passed |
| * as is when {@link #makeCopy()} is called */ |
| private final int mFlags; |
| /** the original content of the layer when the next object was created. This is not |
| * passed in {@link #makeCopy()} and instead is recreated when a new layer is added |
| * (depending on its flags) */ |
| private BufferedImage mOriginalCopy; |
| |
| /** |
| * Creates a layer with a graphics and a bitmap. This is only used to create |
| * the base layer. |
| * |
| * @param graphics the graphics |
| * @param bitmap the bitmap |
| */ |
| Layer(Graphics2D graphics, Bitmap_Delegate bitmap) { |
| mGraphics = graphics; |
| mBitmap = bitmap; |
| mImage = mBitmap.getImage(); |
| mFlags = 0; |
| } |
| |
| /** |
| * Creates a layer with a graphics and an image. If the image belongs to a |
| * {@link Bitmap_Delegate} (case of the base layer), then |
| * {@link Layer#Layer(Graphics2D, Bitmap_Delegate)} should be used. |
| * |
| * @param graphics the graphics the new graphics for this layer |
| * @param image the image the image from which the graphics came |
| * @param flags the flags that were used to save this layer |
| */ |
| Layer(Graphics2D graphics, BufferedImage image, int flags) { |
| mGraphics = graphics; |
| mBitmap = null; |
| mImage = image; |
| mFlags = flags; |
| } |
| |
| /** The Graphics2D, guaranteed to be non null */ |
| Graphics2D getGraphics() { |
| return mGraphics; |
| } |
| |
| /** The BufferedImage, guaranteed to be non null */ |
| BufferedImage getImage() { |
| return mImage; |
| } |
| |
| /** Returns the layer save flags. This is only valid for additional layers. |
| * For the base layer this will always return 0; |
| * For a given layer, all further copies of this {@link Layer} object in new snapshots |
| * will always return the same value. |
| */ |
| int getFlags() { |
| return mFlags; |
| } |
| |
| Layer makeCopy() { |
| if (mBitmap != null) { |
| return new Layer((Graphics2D) mGraphics.create(), mBitmap); |
| } |
| |
| return new Layer((Graphics2D) mGraphics.create(), mImage, mFlags); |
| } |
| |
| /** sets an optional copy of the original content to be used during restore */ |
| void setOriginalCopy(BufferedImage image) { |
| mOriginalCopy = image; |
| } |
| |
| BufferedImage getOriginalCopy() { |
| return mOriginalCopy; |
| } |
| |
| void change() { |
| if (mBitmap != null) { |
| mBitmap.change(); |
| } |
| } |
| |
| /** |
| * Sets the clip for the graphics2D object associated with the layer. |
| * This should be used over the normal Graphics2D setClip method. |
| * |
| * @param clipShape the shape to use a the clip shape. |
| */ |
| void setClip(Shape clipShape) { |
| // because setClip is only guaranteed to work with rectangle shape, |
| // first reset the clip to max and then intersect the current (empty) |
| // clip with the shap. |
| mGraphics.setClip(null); |
| mGraphics.clip(clipShape); |
| } |
| |
| /** |
| * Clips the layer with the given shape. This performs an intersect between the current |
| * clip shape and the given shape. |
| * @param shape the new clip shape. |
| */ |
| public void clip(Shape shape) { |
| mGraphics.clip(shape); |
| } |
| } |
| |
| /** |
| * Creates the root snapshot associating it with a given bitmap. |
| * <p> |
| * If <var>bitmap</var> is null, then {@link GcSnapshot#setBitmap(Bitmap_Delegate)} must be |
| * called before the snapshot can be used to draw. Transform and clip operations are permitted |
| * before. |
| * |
| * @param bitmap the image to associate to the snapshot or null. |
| * @return the root snapshot |
| */ |
| public static GcSnapshot createDefaultSnapshot(Bitmap_Delegate bitmap) { |
| GcSnapshot snapshot = new GcSnapshot(); |
| if (bitmap != null) { |
| snapshot.setBitmap(bitmap); |
| } |
| |
| return snapshot; |
| } |
| |
| /** |
| * Saves the current state according to the given flags and returns the new current snapshot. |
| * <p/> |
| * This is the equivalent of {@link Canvas#save(int)} |
| * |
| * @param flags the save flags. |
| * @return the new snapshot |
| * |
| * @see Canvas#save(int) |
| */ |
| public GcSnapshot save(int flags) { |
| return new GcSnapshot(this, null /*layerbounds*/, null /*paint*/, flags); |
| } |
| |
| /** |
| * Saves the current state and creates a new layer, and returns the new current snapshot. |
| * <p/> |
| * This is the equivalent of {@link Canvas#saveLayer(RectF, Paint, int)} |
| * |
| * @param layerBounds the layer bounds |
| * @param paint the Paint information used to blit the layer back into the layers underneath |
| * upon restore |
| * @param flags the save flags. |
| * @return the new snapshot |
| * |
| * @see Canvas#saveLayer(RectF, Paint, int) |
| */ |
| public GcSnapshot saveLayer(RectF layerBounds, Paint_Delegate paint, int flags) { |
| return new GcSnapshot(this, layerBounds, paint, flags); |
| } |
| |
| /** |
| * Creates the root snapshot. |
| */ |
| private GcSnapshot() { |
| mPrevious = null; |
| mFlags = 0; |
| mLocalLayer = null; |
| mLocalLayerPaint = null; |
| mLayerBounds = null; |
| } |
| |
| /** |
| * Creates a new {@link GcSnapshot} on top of another one, with a layer data to be restored |
| * into the main graphics when {@link #restore()} is called. |
| * |
| * @param previous the previous snapshot head. |
| * @param layerBounds the region of the layer. Optional, if null, this is a normal save() |
| * @param paint the Paint information used to blit the layer back into the layers underneath |
| * upon restore |
| * @param flags the flags regarding what should be saved. |
| */ |
| private GcSnapshot(GcSnapshot previous, RectF layerBounds, Paint_Delegate paint, int flags) { |
| assert previous != null; |
| mPrevious = previous; |
| mFlags = flags; |
| |
| // make a copy of the current layers before adding the new one. |
| // This keeps the same BufferedImage reference but creates new Graphics2D for this |
| // snapshot. |
| // It does not copy whatever original copy the layers have, as they will be done |
| // only if the new layer doesn't clip drawing to itself. |
| for (Layer layer : mPrevious.mLayers) { |
| mLayers.add(layer.makeCopy()); |
| } |
| |
| if (layerBounds != null) { |
| // get the current transform |
| AffineTransform matrix = mLayers.get(0).getGraphics().getTransform(); |
| |
| // transform the layerBounds with the current transform and stores it into a int rect |
| RectF rect2 = new RectF(); |
| mapRect(matrix, rect2, layerBounds); |
| mLayerBounds = new Rect(); |
| rect2.round(mLayerBounds); |
| |
| // get the base layer (always at index 0) |
| Layer baseLayer = mLayers.get(0); |
| |
| // create the image for the layer |
| BufferedImage layerImage = new BufferedImage( |
| baseLayer.getImage().getWidth(), |
| baseLayer.getImage().getHeight(), |
| (mFlags & Canvas.HAS_ALPHA_LAYER_SAVE_FLAG) != 0 ? |
| TYPE_INT_ARGB : |
| TYPE_INT_RGB); |
| |
| // create a graphics for it so that drawing can be done. |
| Graphics2D layerGraphics = layerImage.createGraphics(); |
| |
| // because this layer inherits the current context for transform and clip, |
| // set them to one from the base layer. |
| AffineTransform currentMtx = baseLayer.getGraphics().getTransform(); |
| layerGraphics.setTransform(currentMtx); |
| |
| // create a new layer for this new layer and add it to the list at the end. |
| mLayers.add(mLocalLayer = new Layer(layerGraphics, layerImage, flags)); |
| |
| // set the clip on it. |
| Shape currentClip = baseLayer.getGraphics().getClip(); |
| mLocalLayer.setClip(currentClip); |
| |
| // if the drawing is not clipped to the local layer only, we save the current content |
| // of all other layers. We are only interested in the part that will actually |
| // be drawn, so we create as small bitmaps as we can. |
| // This is so that we can erase the drawing that goes in the layers below that will |
| // be coming from the layer itself. |
| if ((mFlags & Canvas.CLIP_TO_LAYER_SAVE_FLAG) == 0) { |
| int w = mLayerBounds.width(); |
| int h = mLayerBounds.height(); |
| for (int i = 0 ; i < mLayers.size() - 1 ; i++) { |
| Layer layer = mLayers.get(i); |
| BufferedImage image = new BufferedImage(w, h, TYPE_INT_ARGB); |
| Graphics2D graphics = image.createGraphics(); |
| graphics.drawImage(layer.getImage(), |
| 0, 0, w, h, |
| mLayerBounds.left, mLayerBounds.top, |
| mLayerBounds.right, mLayerBounds.bottom, |
| null); |
| graphics.dispose(); |
| layer.setOriginalCopy(image); |
| } |
| } |
| } else { |
| mLocalLayer = null; |
| mLayerBounds = null; |
| } |
| |
| mLocalLayerPaint = paint; |
| } |
| |
| public void dispose() { |
| for (Layer layer : mLayers) { |
| layer.getGraphics().dispose(); |
| } |
| |
| if (mPrevious != null) { |
| mPrevious.dispose(); |
| } |
| } |
| |
| /** |
| * Restores the top {@link GcSnapshot}, and returns the next one. |
| */ |
| public GcSnapshot restore() { |
| return doRestore(); |
| } |
| |
| /** |
| * Restores the {@link GcSnapshot} to <var>saveCount</var>. |
| * @param saveCount the saveCount or -1 to only restore 1. |
| * |
| * @return the new head of the Gc snapshot stack. |
| */ |
| public GcSnapshot restoreTo(int saveCount) { |
| return doRestoreTo(size(), saveCount); |
| } |
| |
| public int size() { |
| if (mPrevious != null) { |
| return mPrevious.size() + 1; |
| } |
| |
| return 1; |
| } |
| |
| /** |
| * Link the snapshot to a Bitmap_Delegate. |
| * <p/> |
| * This is only for the case where the snapshot was created with a null image when calling |
| * {@link #createDefaultSnapshot(Bitmap_Delegate)}, and is therefore not yet linked to |
| * a previous snapshot. |
| * <p/> |
| * If any transform or clip information was set before, they are put into the Graphics object. |
| * @param bitmap the bitmap to link to. |
| */ |
| public void setBitmap(Bitmap_Delegate bitmap) { |
| // create a new Layer for the bitmap. This will be the base layer. |
| Graphics2D graphics2D = bitmap.getImage().createGraphics(); |
| Layer baseLayer = new Layer(graphics2D, bitmap); |
| |
| // Set the current transform and clip which can either come from mTransform/mClip if they |
| // were set when there was no bitmap/layers or from the current base layers if there is |
| // one already. |
| |
| graphics2D.setTransform(getTransform()); |
| // reset mTransform in case there was one. |
| mTransform = null; |
| |
| baseLayer.setClip(getClip()); |
| // reset mClip in case there was one. |
| mClip = null; |
| |
| // replace whatever current layers we have with this. |
| mLayers.clear(); |
| mLayers.add(baseLayer); |
| |
| } |
| |
| public void translate(float dx, float dy) { |
| if (mLayers.size() > 0) { |
| for (Layer layer : mLayers) { |
| layer.getGraphics().translate(dx, dy); |
| } |
| } else { |
| if (mTransform == null) { |
| mTransform = new AffineTransform(); |
| } |
| mTransform.translate(dx, dy); |
| } |
| } |
| |
| public void rotate(double radians) { |
| if (mLayers.size() > 0) { |
| for (Layer layer : mLayers) { |
| layer.getGraphics().rotate(radians); |
| } |
| } else { |
| if (mTransform == null) { |
| mTransform = new AffineTransform(); |
| } |
| mTransform.rotate(radians); |
| } |
| } |
| |
| public void scale(float sx, float sy) { |
| if (mLayers.size() > 0) { |
| for (Layer layer : mLayers) { |
| layer.getGraphics().scale(sx, sy); |
| } |
| } else { |
| if (mTransform == null) { |
| mTransform = new AffineTransform(); |
| } |
| mTransform.scale(sx, sy); |
| } |
| } |
| |
| public AffineTransform getTransform() { |
| if (mLayers.size() > 0) { |
| // all graphics2D in the list have the same transform |
| return mLayers.get(0).getGraphics().getTransform(); |
| } else { |
| if (mTransform == null) { |
| mTransform = new AffineTransform(); |
| } |
| return mTransform; |
| } |
| } |
| |
| public void setTransform(AffineTransform transform) { |
| if (mLayers.size() > 0) { |
| for (Layer layer : mLayers) { |
| layer.getGraphics().setTransform(transform); |
| } |
| } else { |
| if (mTransform == null) { |
| mTransform = new AffineTransform(); |
| } |
| mTransform.setTransform(transform); |
| } |
| } |
| |
| public boolean clip(Shape shape, int regionOp) { |
| // Simple case of intersect with existing layers. |
| // Because Graphics2D#setClip works a bit peculiarly, we optimize |
| // the case of clipping by intersection, as it's supported natively. |
| if (regionOp == Region.Op.INTERSECT.nativeInt && mLayers.size() > 0) { |
| for (Layer layer : mLayers) { |
| layer.clip(shape); |
| } |
| |
| Shape currentClip = getClip(); |
| return currentClip != null && currentClip.getBounds().isEmpty() == false; |
| } |
| |
| Area area = null; |
| |
| if (regionOp == Region.Op.REPLACE.nativeInt) { |
| area = new Area(shape); |
| } else { |
| area = Region_Delegate.combineShapes(getClip(), shape, regionOp); |
| } |
| |
| if (mLayers.size() > 0) { |
| if (area != null) { |
| for (Layer layer : mLayers) { |
| layer.setClip(area); |
| } |
| } |
| |
| Shape currentClip = getClip(); |
| return currentClip != null && currentClip.getBounds().isEmpty() == false; |
| } else { |
| if (area != null) { |
| mClip = area; |
| } else { |
| mClip = new Area(); |
| } |
| |
| return mClip.getBounds().isEmpty() == false; |
| } |
| } |
| |
| public boolean clipRect(float left, float top, float right, float bottom, int regionOp) { |
| return clip(new Rectangle2D.Float(left, top, right - left, bottom - top), regionOp); |
| } |
| |
| /** |
| * Returns the current clip, or null if none have been setup. |
| */ |
| public Shape getClip() { |
| if (mLayers.size() > 0) { |
| // they all have the same clip |
| return mLayers.get(0).getGraphics().getClip(); |
| } else { |
| return mClip; |
| } |
| } |
| |
| private GcSnapshot doRestoreTo(int size, int saveCount) { |
| if (size <= saveCount) { |
| return this; |
| } |
| |
| // restore the current one first. |
| GcSnapshot previous = doRestore(); |
| |
| if (size == saveCount + 1) { // this was the only one that needed restore. |
| return previous; |
| } else { |
| return previous.doRestoreTo(size - 1, saveCount); |
| } |
| } |
| |
| /** |
| * Executes the Drawable's draw method, with a null paint delegate. |
| * <p/> |
| * Note that the method can be called several times if there are more than one active layer. |
| */ |
| public void draw(Drawable drawable) { |
| draw(drawable, null, false /*compositeOnly*/, false /*forceSrcMode*/); |
| } |
| |
| /** |
| * Executes the Drawable's draw method. |
| * <p/> |
| * Note that the method can be called several times if there are more than one active layer. |
| * @param compositeOnly whether the paint is used for composite only. This is typically |
| * the case for bitmaps. |
| * @param forceSrcMode if true, this overrides the composite to be SRC |
| */ |
| public void draw(Drawable drawable, Paint_Delegate paint, boolean compositeOnly, |
| boolean forceSrcMode) { |
| int forceMode = forceSrcMode ? AlphaComposite.SRC : 0; |
| // the current snapshot may not have a mLocalLayer (ie it was created on save() instead |
| // of saveLayer(), but that doesn't mean there's no layer. |
| // mLayers however saves all the information we need (flags). |
| if (mLayers.size() == 1) { |
| // no layer, only base layer. easy case. |
| drawInLayer(mLayers.get(0), drawable, paint, compositeOnly, forceMode); |
| } else { |
| // draw in all the layers until the layer save flags tells us to stop (ie drawing |
| // in that layer is limited to the layer itself. |
| int flags; |
| int i = mLayers.size() - 1; |
| |
| do { |
| Layer layer = mLayers.get(i); |
| |
| drawInLayer(layer, drawable, paint, compositeOnly, forceMode); |
| |
| // then go to previous layer, only if there are any left, and its flags |
| // doesn't restrict drawing to the layer itself. |
| i--; |
| flags = layer.getFlags(); |
| } while (i >= 0 && (flags & Canvas.CLIP_TO_LAYER_SAVE_FLAG) == 0); |
| } |
| } |
| |
| /** |
| * This function calculates a minimum (in area) integer rectangle that contains the input |
| * rectangle after applying to it the affine transform |
| * |
| * @param rect input rectangle |
| * @param transform affine transform applied to the input rectangle |
| * |
| * Returns an output rectangle |
| */ |
| private static Rectangle transformRect(Rectangle rect, AffineTransform transform) { |
| double[] coords = new double[16]; |
| coords[0] = rect.x; |
| coords[1] = rect.y; |
| coords[2] = rect.x + rect.width; |
| coords[3] = rect.y + rect.height; |
| coords[4] = rect.x; |
| coords[5] = rect.y + rect.height; |
| coords[6] = rect.x + rect.width; |
| coords[7] = rect.y; |
| transform.transform(coords, 0, coords, 8, 4); |
| // From 4 transformed vertices of the input rectangle we search for the minimum and maximum |
| // for both coordinates. We round the found extrema to the closest integer, smaller of equal |
| // for the minimums and larger or equal for the maximums. These values represent the border |
| // or the minimum rectangle with sides parallel to the coordinate axis that contains |
| // the transformed rectangle |
| int x = (int) Math.floor(min(min(coords[8], coords[10]), min(coords[12], coords[14]))); |
| int y = (int) Math.floor(min(min(coords[9], coords[11]), min(coords[13], coords[15]))); |
| int w = (int) Math.ceil(max(max(coords[8], coords[10]), max(coords[12], coords[14]))) - x; |
| int h = (int) Math.ceil(max(max(coords[9], coords[11]), max(coords[13], coords[15]))) - y; |
| return new Rectangle(x, y, w, h); |
| } |
| |
| private void drawInLayer(Layer layer, Drawable drawable, Paint_Delegate paint, |
| boolean compositeOnly, int forceMode) { |
| Graphics2D originalGraphics = layer.getGraphics(); |
| if (paint == null) { |
| drawOnGraphics((Graphics2D) originalGraphics.create(), drawable, |
| null /*paint*/, layer); |
| } else { |
| ColorFilter_Delegate filter = paint.getColorFilter(); |
| if (filter == null || !filter.isSupported()) { |
| // get a Graphics2D object configured with the drawing parameters. |
| Graphics2D configuredGraphics = createCustomGraphics(originalGraphics, paint, |
| compositeOnly, forceMode); |
| drawOnGraphics(configuredGraphics, drawable, paint, layer); |
| return; |
| } |
| |
| Rectangle clipBounds = originalGraphics.getClip() != null ? originalGraphics |
| .getClipBounds() : null; |
| AffineTransform transform = originalGraphics.getTransform(); |
| Rectangle imgRect; |
| if (clipBounds != null) { |
| if (clipBounds.width == 0 || clipBounds.height == 0) { |
| // Clip is 0 so no need to paint anything. |
| return; |
| } |
| // Calculate integer rectangle that contains clipBounds after the transform, that is |
| // the minimum image size we can use to render the drawable |
| imgRect = transformRect(clipBounds, transform); |
| transform = new AffineTransform( |
| transform.getScaleX(), |
| transform.getShearY(), |
| transform.getShearX(), |
| transform.getScaleY(), |
| transform.getTranslateX() - imgRect.x, |
| transform.getTranslateY() - imgRect.y); |
| } else { |
| imgRect = |
| new Rectangle( |
| 0, 0, layer.getImage().getWidth(), layer.getImage().getHeight()); |
| } |
| |
| // Create a temporary image to which the color filter will be applied. |
| BufferedImage image = new BufferedImage(imgRect.width, imgRect.height, TYPE_INT_ARGB); |
| Graphics2D imageBaseGraphics = (Graphics2D) image.getGraphics(); |
| // Configure the Graphics2D object with drawing parameters and shader. |
| Graphics2D imageGraphics = createCustomGraphics( |
| imageBaseGraphics, paint, compositeOnly, |
| AlphaComposite.SRC_OVER); |
| |
| // get a Graphics2D object configured with the drawing parameters, but no shader. |
| Graphics2D configuredGraphics = createCustomGraphics(originalGraphics, paint, |
| true /*compositeOnly*/, forceMode); |
| configuredGraphics.setTransform(IDENTITY_TRANSFORM); |
| try { |
| // The main draw operation. |
| // We translate the operation to take into account that the rendering does not |
| // know about the clipping area. |
| imageGraphics.setTransform(transform); |
| drawable.draw(imageGraphics, paint); |
| |
| // Apply the color filter. |
| // Restore the original coordinates system and apply the filter only to the |
| // clipped area. |
| imageGraphics.setTransform(IDENTITY_TRANSFORM); |
| filter.applyFilter(imageGraphics, imgRect.width, imgRect.height); |
| |
| // Draw the tinted image on the main layer using as start point the clipping |
| // upper left coordinates. |
| configuredGraphics.drawImage(image, imgRect.x, imgRect.y, null); |
| layer.change(); |
| } finally { |
| // dispose Graphics2D objects |
| imageGraphics.dispose(); |
| imageBaseGraphics.dispose(); |
| configuredGraphics.dispose(); |
| } |
| } |
| } |
| |
| private void drawOnGraphics(Graphics2D g, Drawable drawable, Paint_Delegate paint, |
| Layer layer) { |
| try { |
| drawable.draw(g, paint); |
| layer.change(); |
| } finally { |
| g.dispose(); |
| } |
| } |
| |
| private GcSnapshot doRestore() { |
| if (mPrevious != null) { |
| if (mLocalLayer != null) { |
| // prepare to blit the layers in which we have draw, in the layer beneath |
| // them, starting with the top one (which is the current local layer). |
| int i = mLayers.size() - 1; |
| int flags; |
| do { |
| Layer dstLayer = mLayers.get(i - 1); |
| |
| restoreLayer(dstLayer); |
| |
| flags = dstLayer.getFlags(); |
| i--; |
| } while (i > 0 && (flags & Canvas.CLIP_TO_LAYER_SAVE_FLAG) == 0); |
| } |
| |
| // if this snapshot does not save everything, then set the previous snapshot |
| // to this snapshot content |
| |
| // didn't save the matrix? set the current matrix on the previous snapshot |
| if ((mFlags & Canvas.MATRIX_SAVE_FLAG) == 0) { |
| AffineTransform mtx = getTransform(); |
| for (Layer layer : mPrevious.mLayers) { |
| layer.getGraphics().setTransform(mtx); |
| } |
| } |
| |
| // didn't save the clip? set the current clip on the previous snapshot |
| if ((mFlags & Canvas.CLIP_SAVE_FLAG) == 0) { |
| Shape clip = getClip(); |
| for (Layer layer : mPrevious.mLayers) { |
| layer.setClip(clip); |
| } |
| } |
| } |
| |
| for (Layer layer : mLayers) { |
| layer.getGraphics().dispose(); |
| } |
| |
| return mPrevious; |
| } |
| |
| private void restoreLayer(Layer dstLayer) { |
| |
| Graphics2D baseGfx = dstLayer.getImage().createGraphics(); |
| |
| // if the layer contains an original copy this means the flags |
| // didn't restrict drawing to the local layer and we need to make sure the |
| // layer bounds in the layer beneath didn't receive any drawing. |
| // so we use the originalCopy to erase the new drawings in there. |
| BufferedImage originalCopy = dstLayer.getOriginalCopy(); |
| if (originalCopy != null) { |
| Graphics2D g = (Graphics2D) baseGfx.create(); |
| g.setComposite(AlphaComposite.Src); |
| |
| g.drawImage(originalCopy, |
| mLayerBounds.left, mLayerBounds.top, mLayerBounds.right, mLayerBounds.bottom, |
| 0, 0, mLayerBounds.width(), mLayerBounds.height(), |
| null); |
| g.dispose(); |
| } |
| |
| // now draw put the content of the local layer onto the layer, |
| // using the paint information |
| Graphics2D g = createCustomGraphics(baseGfx, mLocalLayerPaint, |
| true /*alphaOnly*/, 0 /*forceMode*/); |
| |
| g.drawImage(mLocalLayer.getImage(), |
| mLayerBounds.left, mLayerBounds.top, mLayerBounds.right, mLayerBounds.bottom, |
| mLayerBounds.left, mLayerBounds.top, mLayerBounds.right, mLayerBounds.bottom, |
| null); |
| g.dispose(); |
| |
| baseGfx.dispose(); |
| } |
| |
| /** |
| * Creates a new {@link Graphics2D} based on the {@link Paint} parameters. |
| * <p/>The object must be disposed ({@link Graphics2D#dispose()}) after being used. |
| */ |
| private Graphics2D createCustomGraphics(Graphics2D original, Paint_Delegate paint, |
| boolean compositeOnly, int forceMode) { |
| // make new one graphics |
| Graphics2D g = (Graphics2D) original.create(); |
| |
| if (paint == null) { |
| return g; |
| } |
| |
| // configure it |
| |
| if (paint.isAntiAliased()) { |
| g.setRenderingHint( |
| RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); |
| g.setRenderingHint( |
| RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); |
| } |
| |
| // set the shader first, as it'll replace the color if it can be used it. |
| if (!compositeOnly) { |
| setShader(g, paint); |
| // set the stroke |
| g.setStroke(paint.getJavaStroke()); |
| } |
| // set the composite. |
| setComposite(g, paint, compositeOnly, forceMode); |
| |
| return g; |
| } |
| |
| private void setShader(Graphics2D g, Paint_Delegate paint) { |
| Shader_Delegate shaderDelegate = paint.getShader(); |
| if (shaderDelegate != null) { |
| if (shaderDelegate.isSupported()) { |
| java.awt.Paint shaderPaint = shaderDelegate.getJavaPaint(); |
| assert shaderPaint != null; |
| g.setPaint(shaderPaint); |
| return; |
| } else { |
| Bridge.getLog().fidelityWarning(LayoutLog.TAG_SHADER, |
| shaderDelegate.getSupportMessage(), null, null, null); |
| } |
| } |
| |
| // if no shader, use the paint color |
| g.setColor(new Color(paint.getColor(), true /*hasAlpha*/)); |
| } |
| |
| private void setComposite(Graphics2D g, Paint_Delegate paint, boolean usePaintAlpha, |
| int forceMode) { |
| // the alpha for the composite. Always opaque if the normal paint color is used since |
| // it contains the alpha |
| int alpha = usePaintAlpha ? paint.getAlpha() : 0xFF; |
| Shader_Delegate shader = paint.getShader(); |
| if (shader != null) { |
| alpha = (int)(alpha * shader.getAlpha()); |
| } |
| if (forceMode != 0) { |
| g.setComposite(AlphaComposite.getInstance(forceMode, (float) alpha / 255.f)); |
| return; |
| } |
| Mode mode = PorterDuff.intToMode(paint.getPorterDuffMode()); |
| Composite composite = PorterDuffUtility.getComposite(mode, alpha); |
| g.setComposite(composite); |
| } |
| |
| private void mapRect(AffineTransform matrix, RectF dst, RectF src) { |
| // array with 4 corners |
| float[] corners = new float[] { |
| src.left, src.top, |
| src.right, src.top, |
| src.right, src.bottom, |
| src.left, src.bottom, |
| }; |
| |
| // apply the transform to them. |
| matrix.transform(corners, 0, corners, 0, 4); |
| |
| // now put the result in the rect. We take the min/max of Xs and min/max of Ys |
| dst.left = Math.min(Math.min(corners[0], corners[2]), Math.min(corners[4], corners[6])); |
| dst.right = Math.max(Math.max(corners[0], corners[2]), Math.max(corners[4], corners[6])); |
| |
| dst.top = Math.min(Math.min(corners[1], corners[3]), Math.min(corners[5], corners[7])); |
| dst.bottom = Math.max(Math.max(corners[1], corners[3]), Math.max(corners[5], corners[7])); |
| } |
| |
| /** |
| * Returns the clip of the oldest snapshot of the stack, appropriately translated to be |
| * expressed in the coordinate system of the latest snapshot. |
| */ |
| public Rectangle getOriginalClip() { |
| GcSnapshot originalSnapshot = this; |
| while (originalSnapshot.mPrevious != null) { |
| originalSnapshot = originalSnapshot.mPrevious; |
| } |
| if (originalSnapshot.mLayers.isEmpty()) { |
| return null; |
| } |
| Graphics2D graphics2D = originalSnapshot.mLayers.get(0).getGraphics(); |
| Rectangle bounds = graphics2D.getClipBounds(); |
| if (bounds == null) { |
| return null; |
| } |
| try { |
| AffineTransform originalTransform = |
| ((Graphics2D) graphics2D.create()).getTransform().createInverse(); |
| AffineTransform latestTransform = getTransform().createInverse(); |
| bounds.x += latestTransform.getTranslateX() - originalTransform.getTranslateX(); |
| bounds.y += latestTransform.getTranslateY() - originalTransform.getTranslateY(); |
| } catch (NoninvertibleTransformException e) { |
| Bridge.getLog().warning(null, "Non invertible transformation", null, null); |
| } |
| return bounds; |
| } |
| |
| } |