GcSnapshot.java revision d38e776a3cc8cb53945cbebafbe6f6c2e3501fa5
1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.layoutlib.bridge.impl;
18
19import com.android.layoutlib.bridge.Bridge;
20
21import android.graphics.Canvas;
22import android.graphics.Paint;
23import android.graphics.Paint_Delegate;
24import android.graphics.PathEffect_Delegate;
25import android.graphics.PorterDuff;
26import android.graphics.PorterDuffXfermode_Delegate;
27import android.graphics.Rect;
28import android.graphics.RectF;
29import android.graphics.Region;
30import android.graphics.Shader_Delegate;
31import android.graphics.Xfermode_Delegate;
32
33import java.awt.AlphaComposite;
34import java.awt.BasicStroke;
35import java.awt.Color;
36import java.awt.Composite;
37import java.awt.Graphics2D;
38import java.awt.RenderingHints;
39import java.awt.Shape;
40import java.awt.Stroke;
41import java.awt.geom.AffineTransform;
42import java.awt.geom.Area;
43import java.awt.geom.Rectangle2D;
44import java.awt.image.BufferedImage;
45import java.util.ArrayList;
46
47/**
48 * Class representing a graphics context snapshot, as well as a context stack as a linked list.
49 * <p>
50 * This is based on top of {@link Graphics2D} but can operate independently if none are available
51 * yet when setting transforms and clip information.
52 * <p>
53 * This allows for drawing through {@link #draw(Drawable, Paint_Delegate)} and
54 * {@link #draw(Drawable, Paint_Delegate)}
55 *
56 */
57public class GcSnapshot {
58
59    private final GcSnapshot mPrevious;
60    private final int mFlags;
61
62    /** list of layers. */
63    private final ArrayList<Layer> mLayers = new ArrayList<Layer>();
64
65    /** temp transform in case transformation are set before a Graphics2D exists */
66    private AffineTransform mTransform = null;
67    /** temp clip in case clipping is set before a Graphics2D exists */
68    private Area mClip = null;
69
70    // local layer data
71    private final Layer mLocalLayer;
72    private final Paint_Delegate mLocalLayerPaint;
73    private Rect mLocalLayerRegion;
74
75    public interface Drawable {
76        void draw(Graphics2D graphics, Paint_Delegate paint);
77    }
78
79    /**
80     * class containing information about a layer.
81     */
82    private static class Layer {
83        private final Graphics2D mGraphics;
84        private final BufferedImage mImage;
85        private BufferedImage mOriginalCopy;
86
87        Layer(Graphics2D graphics, BufferedImage image) {
88            mGraphics = graphics;
89            mImage = image;
90        }
91
92        /** The Graphics2D, guaranteed to be non null */
93        Graphics2D getGraphics() {
94            return mGraphics;
95        }
96
97        /** The BufferedImage, guaranteed to be non null */
98        BufferedImage getImage() {
99            return mImage;
100        }
101
102        Layer makeCopy() {
103            return new Layer((Graphics2D) mGraphics.create(), mImage);
104        }
105
106        /** sets an optional copy of the original content to be used during restore */
107        void setOriginalCopy(BufferedImage image) {
108            mOriginalCopy = image;
109        }
110
111        BufferedImage getOriginalCopy() {
112            return mOriginalCopy;
113        }
114    }
115
116    /**
117     * Creates the root snapshot associating it with a given image.
118     * <p>
119     * If <var>image</var> is null, then {@link GcSnapshot#setImage(BufferedImage)} must be
120     * called before the snapshot can be used to draw. Transform and clip operations are permitted
121     * before.
122     *
123     * @param image the image to associate to the snapshot or null.
124     * @return the root snapshot
125     */
126    public static GcSnapshot createDefaultSnapshot(BufferedImage image) {
127        GcSnapshot snapshot = new GcSnapshot();
128        if (image != null) {
129            snapshot.setImage(image);
130        }
131
132        return snapshot;
133    }
134
135    /**
136     * Saves the current state according to the given flags and returns the new current snapshot.
137     * <p/>
138     * This is the equivalent of {@link Canvas#save(int)}
139     *
140     * @param flags the save flags.
141     * @return the new snapshot
142     *
143     * @see Canvas#save(int)
144     */
145    public GcSnapshot save(int flags) {
146        return new GcSnapshot(this, null /*layerbounds*/, null /*paint*/, flags);
147    }
148
149    /**
150     * Saves the current state and creates a new layer, and returns the new current snapshot.
151     * <p/>
152     * This is the equivalent of {@link Canvas#saveLayer(RectF, Paint, int)}
153     *
154     * @param layerBounds the layer bounds
155     * @param paint the Paint information used to blit the layer back into the layers underneath
156     *          upon restore
157     * @param flags the save flags.
158     * @return the new snapshot
159     *
160     * @see Canvas#saveLayer(RectF, Paint, int)
161     */
162    public GcSnapshot saveLayer(RectF layerBounds, Paint_Delegate paint, int flags) {
163        return new GcSnapshot(this, layerBounds, paint, flags);
164    }
165
166    /**
167     * Creates the root snapshot.
168     * {@link #setGraphics2D(Graphics2D)} will have to be called on it when possible.
169     */
170    private GcSnapshot() {
171        mPrevious = null;
172        mFlags = 0;
173        mLocalLayer = null;
174        mLocalLayerPaint = null;
175    }
176
177    /**
178     * Creates a new {@link GcSnapshot} on top of another one, with a layer data to be restored
179     * into the main graphics when {@link #restore()} is called.
180     *
181     * @param previous the previous snapshot head.
182     * @param layerBounds the region of the layer. Optional, if null, this is a normal save()
183     * @param paint the Paint information used to blit the layer back into the layers underneath
184     *          upon restore
185     * @param flags the flags regarding what should be saved.
186     */
187    private GcSnapshot(GcSnapshot previous, RectF layerBounds, Paint_Delegate paint, int flags) {
188        assert previous != null;
189        mPrevious = previous;
190        mFlags = flags;
191
192        // make a copy of the current layers before adding the new one.
193        // This keeps the same BufferedImage reference but creates new Graphics2D for this
194        // snapshot.
195        // It does not copy whatever original copy the layers have, as they will be done
196        // only if the new layer doesn't clip drawing to itself.
197        for (Layer layer : mPrevious.mLayers) {
198            mLayers.add(layer.makeCopy());
199        }
200
201        if (layerBounds != null) {
202            // get the current transform
203            AffineTransform matrix = mLayers.get(0).getGraphics().getTransform();
204
205            // transform the layerBounds and puts it into a int rect
206            RectF rect2 = new RectF();
207            mapRect(matrix, rect2, layerBounds);
208            mLocalLayerRegion = new Rect();
209            rect2.round(mLocalLayerRegion);
210
211            // get the base layer (always at index 0)
212            Layer baseLayer = mLayers.get(0);
213
214            // create the image for the layer
215            BufferedImage layerImage = new BufferedImage(
216                    baseLayer.getImage().getWidth(),
217                    baseLayer.getImage().getHeight(),
218                    BufferedImage.TYPE_INT_ARGB);
219
220            // create a graphics for it so that drawing can be done.
221            Graphics2D layerGraphics = layerImage.createGraphics();
222
223            // because this layer inherits the current context for transform and clip,
224            // set them to one from the base layer.
225            AffineTransform currentMtx = baseLayer.getGraphics().getTransform();
226            layerGraphics.setTransform(currentMtx);
227
228            Shape currentClip = baseLayer.getGraphics().getClip();
229            layerGraphics.setClip(currentClip);
230
231            // create a new layer for this new layer and add it to the list at the end.
232            mLayers.add(mLocalLayer = new Layer(layerGraphics, layerImage));
233
234            // if the drawing is not clipped to the local layer only, we save the current content
235            // of all other layers. We are only interested in the part that will actually
236            // be drawn, so we create as small bitmaps as we can.
237            // This is so that we can erase the drawing that goes in the layers below that will
238            // be coming from the layer itself.
239            if ((mFlags & Canvas.CLIP_TO_LAYER_SAVE_FLAG) == 0) {
240                int w = mLocalLayerRegion.width();
241                int h = mLocalLayerRegion.height();
242                for (int i = 0 ; i < mLayers.size() - 1 ; i++) {
243                    Layer layer = mLayers.get(i);
244                    BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
245                    Graphics2D graphics = image.createGraphics();
246                    graphics.drawImage(layer.getImage(),
247                            0, 0, w, h,
248                            mLocalLayerRegion.left, mLocalLayerRegion.top,
249                                    mLocalLayerRegion.right, mLocalLayerRegion.bottom,
250                            null);
251                    graphics.dispose();
252                    layer.setOriginalCopy(image);
253                }
254            }
255        } else {
256            mLocalLayer = null;
257        }
258
259        mLocalLayerPaint  = paint;
260    }
261
262    public void dispose() {
263        for (Layer layer : mLayers) {
264            layer.getGraphics().dispose();
265        }
266
267        if (mPrevious != null) {
268            mPrevious.dispose();
269        }
270    }
271
272    /**
273     * Restores the top {@link GcSnapshot}, and returns the next one.
274     */
275    public GcSnapshot restore() {
276        return doRestore();
277    }
278
279    /**
280     * Restores the {@link GcSnapshot} to <var>saveCount</var>.
281     * @param saveCount the saveCount or -1 to only restore 1.
282     *
283     * @return the new head of the Gc snapshot stack.
284     */
285    public GcSnapshot restoreTo(int saveCount) {
286        return doRestoreTo(size(), saveCount);
287    }
288
289    public int size() {
290        if (mPrevious != null) {
291            return mPrevious.size() + 1;
292        }
293
294        return 1;
295    }
296
297    /**
298     * Link the snapshot to a BufferedImage.
299     * <p/>
300     * This is only for the case where the snapshot was created with a null image when calling
301     * {@link #createDefaultSnapshot(BufferedImage)}, and is therefore not yet linked to
302     * a previous snapshot.
303     * <p/>
304     * If any transform or clip information was set before, they are put into the Graphics object.
305     * @param image the buffered image to link to.
306     */
307    public void setImage(BufferedImage image) {
308        assert mLayers.size() == 0;
309        Graphics2D graphics2D = image.createGraphics();
310        mLayers.add(new Layer(graphics2D, image));
311        if (mTransform != null) {
312            graphics2D.setTransform(mTransform);
313            mTransform = null;
314        }
315
316        if (mClip != null) {
317            graphics2D.setClip(mClip);
318            mClip = null;
319        }
320    }
321
322    public void translate(float dx, float dy) {
323        if (mLayers.size() > 0) {
324            for (Layer layer : mLayers) {
325                layer.getGraphics().translate(dx, dy);
326            }
327        } else {
328            if (mTransform == null) {
329                mTransform = new AffineTransform();
330            }
331            mTransform.translate(dx, dy);
332        }
333    }
334
335    public void rotate(double radians) {
336        if (mLayers.size() > 0) {
337            for (Layer layer : mLayers) {
338                layer.getGraphics().rotate(radians);
339            }
340        } else {
341            if (mTransform == null) {
342                mTransform = new AffineTransform();
343            }
344            mTransform.rotate(radians);
345        }
346    }
347
348    public void scale(float sx, float sy) {
349        if (mLayers.size() > 0) {
350            for (Layer layer : mLayers) {
351                layer.getGraphics().scale(sx, sy);
352            }
353        } else {
354            if (mTransform == null) {
355                mTransform = new AffineTransform();
356            }
357            mTransform.scale(sx, sy);
358        }
359    }
360
361    public AffineTransform getTransform() {
362        if (mLayers.size() > 0) {
363            // all graphics2D in the list have the same transform
364            return mLayers.get(0).getGraphics().getTransform();
365        } else {
366            if (mTransform == null) {
367                mTransform = new AffineTransform();
368            }
369            return mTransform;
370        }
371    }
372
373    public void setTransform(AffineTransform transform) {
374        if (mLayers.size() > 0) {
375            for (Layer layer : mLayers) {
376                layer.getGraphics().setTransform(transform);
377            }
378        } else {
379            if (mTransform == null) {
380                mTransform = new AffineTransform();
381            }
382            mTransform.setTransform(transform);
383        }
384    }
385
386    public boolean clipRect(float left, float top, float right, float bottom, int regionOp) {
387        if (mLayers.size() > 0) {
388            Shape clip = null;
389            if (regionOp == Region.Op.DIFFERENCE.nativeInt) {
390                Area newClip = new Area(getClip());
391                newClip.subtract(new Area(
392                        new Rectangle2D.Float(left, top, right - left, bottom - top)));
393                clip = newClip;
394
395            } else if (regionOp == Region.Op.INTERSECT.nativeInt) {
396                for (Layer layer : mLayers) {
397                    layer.getGraphics().clipRect(
398                            (int) left, (int) top, (int) (right - left), (int) (bottom - top));
399                }
400
401            } else if (regionOp == Region.Op.UNION.nativeInt) {
402                Area newClip = new Area(getClip());
403                newClip.add(new Area(
404                        new Rectangle2D.Float(left, top, right - left, bottom - top)));
405                clip = newClip;
406
407            } else if (regionOp == Region.Op.XOR.nativeInt) {
408                Area newClip = new Area(getClip());
409                newClip.exclusiveOr(new Area(
410                        new Rectangle2D.Float(left, top, right - left, bottom - top)));
411                clip = newClip;
412
413            } else if (regionOp == Region.Op.REVERSE_DIFFERENCE.nativeInt) {
414                Area newClip = new Area(
415                        new Rectangle2D.Float(left, top, right - left, bottom - top));
416                newClip.subtract(new Area(getClip()));
417                clip = newClip;
418
419            } else if (regionOp == Region.Op.REPLACE.nativeInt) {
420                for (Layer layer : mLayers) {
421                    layer.getGraphics().setClip(
422                            (int) left, (int) top, (int) (right - left), (int) (bottom - top));
423                }
424            }
425
426            if (clip != null) {
427                for (Layer layer : mLayers) {
428                    layer.getGraphics().setClip(clip);
429                }
430            }
431
432            return getClip().getBounds().isEmpty() == false;
433        } else {
434            if (mClip == null) {
435                mClip = new Area();
436            }
437
438            if (regionOp == Region.Op.DIFFERENCE.nativeInt) {
439                //FIXME
440            } else if (regionOp == Region.Op.DIFFERENCE.nativeInt) {
441            } else if (regionOp == Region.Op.INTERSECT.nativeInt) {
442            } else if (regionOp == Region.Op.UNION.nativeInt) {
443            } else if (regionOp == Region.Op.XOR.nativeInt) {
444            } else if (regionOp == Region.Op.REVERSE_DIFFERENCE.nativeInt) {
445            } else if (regionOp == Region.Op.REPLACE.nativeInt) {
446            }
447
448            return mClip.getBounds().isEmpty() == false;
449        }
450    }
451
452    public Shape getClip() {
453        if (mLayers.size() > 0) {
454            // they all have the same clip
455            return mLayers.get(0).getGraphics().getClip();
456        } else {
457            if (mClip == null) {
458                mClip = new Area();
459            }
460            return mClip;
461        }
462    }
463
464    private GcSnapshot doRestoreTo(int size, int saveCount) {
465        if (size <= saveCount) {
466            return this;
467        }
468
469        // restore the current one first.
470        GcSnapshot previous = doRestore();
471
472        if (size == saveCount + 1) { // this was the only one that needed restore.
473            return previous;
474        } else {
475            return previous.doRestoreTo(size - 1, saveCount);
476        }
477    }
478
479    /**
480     * Executes the Drawable's draw method, with a null paint delegate.
481     * <p/>
482     * Note that the method can be called several times if there are more than one active layer.
483     * @param drawable
484     */
485    public void draw(Drawable drawable) {
486        draw(drawable, null, false /*compositeOnly*/);
487    }
488
489    /**
490     * Executes the Drawable's draw method.
491     * <p/>
492     * Note that the method can be called several times if there are more than one active layer.
493     * @param drawable
494     * @param paint
495     * @param compositeOnly whether the paint is used for composite only. This is typically
496     *          the case for bitmaps.
497     */
498    public void draw(Drawable drawable, Paint_Delegate paint, boolean compositeOnly) {
499        // if we clip to the layer, then we only draw in the layer
500        if (mLocalLayer != null && (mFlags & Canvas.CLIP_TO_LAYER_SAVE_FLAG) != 0) {
501            drawInLayer(mLocalLayer, drawable, paint, compositeOnly);
502        } else {
503            // draw in all the layers
504            for (Layer layer : mLayers) {
505                drawInLayer(layer, drawable, paint, compositeOnly);
506            }
507        }
508    }
509
510    private void drawInLayer(Layer layer, Drawable drawable, Paint_Delegate paint,
511            boolean compositeOnly) {
512        Graphics2D originalGraphics = layer.getGraphics();
513        // get a Graphics2D object configured with the drawing parameters.
514        Graphics2D configuredGraphics2D =
515            paint != null ?
516                    createCustomGraphics(originalGraphics, paint, compositeOnly) :
517                        (Graphics2D) originalGraphics.create();
518
519        try {
520            drawable.draw(configuredGraphics2D, paint);
521        } finally {
522            // dispose Graphics2D object
523            configuredGraphics2D.dispose();
524        }
525    }
526
527    private GcSnapshot doRestore() {
528        if (mPrevious != null) {
529            boolean forceAllSave = false;
530            if (mLocalLayer != null) {
531                forceAllSave = true; // layers always save both clip and transform
532
533                // prepare to draw the current layer in the previous layers, ie all layers but
534                // the last one, since the last one is the local layer
535                for (int i = 0 ; i < mLayers.size() - 1 ; i++) {
536                    Layer layer = mLayers.get(i);
537
538                    Graphics2D baseGfx = layer.getImage().createGraphics();
539
540                    // if the layer contains an original copy this means the flags
541                    // didn't restrict drawing to the local layer and we need to make sure the
542                    // layer bounds in the layer beneath didn't receive any drawing.
543                    // so we use the originalCopy to erase the new drawings in there.
544                    BufferedImage originalCopy = layer.getOriginalCopy();
545                    if (originalCopy != null) {
546                        Graphics2D g = (Graphics2D) baseGfx.create();
547                        g.setComposite(AlphaComposite.Src);
548
549                        g.drawImage(originalCopy,
550                                mLocalLayerRegion.left, mLocalLayerRegion.top,
551                                        mLocalLayerRegion.right, mLocalLayerRegion.bottom,
552                                0, 0, mLocalLayerRegion.width(), mLocalLayerRegion.height(),
553                                null);
554                        g.dispose();
555                    }
556
557                    // now draw put the content of the local layer onto the layer, using the paint
558                    // information
559                    Graphics2D g = createCustomGraphics(baseGfx, mLocalLayerPaint,
560                            true /*alphaOnly*/);
561
562                    g.drawImage(mLocalLayer.getImage(),
563                            mLocalLayerRegion.left, mLocalLayerRegion.top,
564                                    mLocalLayerRegion.right, mLocalLayerRegion.bottom,
565                            mLocalLayerRegion.left, mLocalLayerRegion.top,
566                                    mLocalLayerRegion.right, mLocalLayerRegion.bottom,
567                            null);
568                    g.dispose();
569
570                    baseGfx.dispose();
571                }
572            }
573
574            // if this snapshot does not save everything, then set the previous snapshot
575            // to this snapshot content
576
577            // didn't save the matrix? set the current matrix on the previous snapshot
578            if (forceAllSave == false && (mFlags & Canvas.MATRIX_SAVE_FLAG) == 0) {
579                AffineTransform mtx = getTransform();
580                for (Layer layer : mPrevious.mLayers) {
581                    layer.getGraphics().setTransform(mtx);
582                }
583            }
584
585            // didn't save the clip? set the current clip on the previous snapshot
586            if (forceAllSave == false && (mFlags & Canvas.CLIP_SAVE_FLAG) == 0) {
587                Shape clip = getClip();
588                for (Layer layer : mPrevious.mLayers) {
589                    layer.getGraphics().setClip(clip);
590                }
591            }
592        }
593
594        for (Layer layer : mLayers) {
595            layer.getGraphics().dispose();
596        }
597
598        return mPrevious;
599    }
600
601    /**
602     * Creates a new {@link Graphics2D} based on the {@link Paint} parameters.
603     * <p/>The object must be disposed ({@link Graphics2D#dispose()}) after being used.
604     */
605    private Graphics2D createCustomGraphics(Graphics2D original, Paint_Delegate paint,
606            boolean compositeOnly) {
607        // make new one graphics
608        Graphics2D g = (Graphics2D) original.create();
609
610        // configure it
611
612        if (paint.isAntiAliased()) {
613            g.setRenderingHint(
614                    RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
615            g.setRenderingHint(
616                    RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
617        }
618
619        boolean customShader = false;
620
621        // get the shader first, as it'll replace the color if it can be used it.
622        if (compositeOnly == false) {
623            int nativeShader = paint.getShader();
624            if (nativeShader > 0) {
625                Shader_Delegate shaderDelegate = Shader_Delegate.getDelegate(nativeShader);
626                assert shaderDelegate != null;
627                if (shaderDelegate != null) {
628                    if (shaderDelegate.isSupported()) {
629                        java.awt.Paint shaderPaint = shaderDelegate.getJavaPaint();
630                        assert shaderPaint != null;
631                        if (shaderPaint != null) {
632                            g.setPaint(shaderPaint);
633                            customShader = true;
634                        }
635                    } else {
636                        Bridge.getLog().fidelityWarning(null,
637                                shaderDelegate.getSupportMessage(),
638                                null);
639                    }
640                }
641            }
642
643            // if no shader, use the paint color
644            if (customShader == false) {
645                g.setColor(new Color(paint.getColor(), true /*hasAlpha*/));
646            }
647
648            boolean customStroke = false;
649            int pathEffect = paint.getPathEffect();
650            if (pathEffect > 0) {
651                PathEffect_Delegate effectDelegate = PathEffect_Delegate.getDelegate(pathEffect);
652                assert effectDelegate != null;
653                if (effectDelegate != null) {
654                    if (effectDelegate.isSupported()) {
655                        Stroke stroke = effectDelegate.getStroke(paint);
656                        assert stroke != null;
657                        if (stroke != null) {
658                            g.setStroke(stroke);
659                            customStroke = true;
660                        }
661                    } else {
662                        Bridge.getLog().fidelityWarning(null,
663                                effectDelegate.getSupportMessage(),
664                                null);
665                    }
666                }
667            }
668
669            // if no custom stroke as been set, set the default one.
670            if (customStroke == false) {
671                g.setStroke(new BasicStroke(
672                        paint.getStrokeWidth(),
673                        paint.getJavaCap(),
674                        paint.getJavaJoin(),
675                        paint.getJavaStrokeMiter()));
676            }
677        }
678
679        // the alpha for the composite. Always opaque if the normal paint color is used since
680        // it contains the alpha
681        int alpha = (compositeOnly || customShader) ? paint.getAlpha() : 0xFF;
682
683        boolean customXfermode = false;
684        int xfermode = paint.getXfermode();
685        if (xfermode > 0) {
686            Xfermode_Delegate xfermodeDelegate = Xfermode_Delegate.getDelegate(paint.getXfermode());
687            assert xfermodeDelegate != null;
688            if (xfermodeDelegate != null) {
689                if (xfermodeDelegate.isSupported()) {
690                    Composite composite = xfermodeDelegate.getComposite(alpha);
691                    assert composite != null;
692                    if (composite != null) {
693                        g.setComposite(composite);
694                        customXfermode = true;
695                    }
696                } else {
697                    Bridge.getLog().fidelityWarning(null,
698                            xfermodeDelegate.getSupportMessage(),
699                            null);
700                }
701            }
702        }
703
704        // if there was no custom xfermode, but we have alpha (due to a shader and a non
705        // opaque alpha channel in the paint color), then we create an AlphaComposite anyway
706        // that will handle the alpha.
707        if (customXfermode == false && alpha != 0xFF) {
708            g.setComposite(PorterDuffXfermode_Delegate.getComposite(
709                    PorterDuff.Mode.SRC_OVER, alpha));
710        }
711
712        return g;
713    }
714
715
716    private void mapRect(AffineTransform matrix, RectF dst, RectF src) {
717        // array with 4 corners
718        float[] corners = new float[] {
719                src.left, src.top,
720                src.right, src.top,
721                src.right, src.bottom,
722                src.left, src.bottom,
723        };
724
725        // apply the transform to them.
726        matrix.transform(corners, 0, corners, 0, 4);
727
728        // now put the result in the rect. We take the min/max of Xs and min/max of Ys
729        dst.left = Math.min(Math.min(corners[0], corners[2]), Math.min(corners[4], corners[6]));
730        dst.right = Math.max(Math.max(corners[0], corners[2]), Math.max(corners[4], corners[6]));
731
732        dst.top = Math.min(Math.min(corners[1], corners[3]), Math.min(corners[5], corners[7]));
733        dst.bottom = Math.max(Math.max(corners[1], corners[3]), Math.max(corners[5], corners[7]));
734    }
735
736}
737