ADT/Layout: support for 3+ color in linear gradients

Change-Id: I14c6a5a1de41470c6f1c66d490492ecc727302f2
diff --git a/tools/layoutlib/bridge/src/android/graphics/LinearGradient.java b/tools/layoutlib/bridge/src/android/graphics/LinearGradient.java
index 7cb8f26..945a539 100644
--- a/tools/layoutlib/bridge/src/android/graphics/LinearGradient.java
+++ b/tools/layoutlib/bridge/src/android/graphics/LinearGradient.java
@@ -19,10 +19,20 @@
 import java.awt.GradientPaint;
 import java.awt.Color;
 import java.awt.Paint;
+import java.awt.PaintContext;
+import java.awt.Rectangle;
+import java.awt.RenderingHints;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Rectangle2D;
+import java.awt.image.ColorModel;
+import java.awt.image.DataBuffer;
+import java.awt.image.Raster;
+import java.awt.image.SampleModel;
+import java.awt.image.WritableRaster;
 
 public class LinearGradient extends Shader {
 
-    private GradientPaint mGradientPaint;
+    private Paint mJavaPaint;
 
     /**
      * Create a shader that draws a linear gradient along a line.
@@ -46,12 +56,13 @@
             throw new IllegalArgumentException("color and position arrays must be of equal length");
         }
 
-        // FIXME implement multi color linear gradient
-        if (colors.length == 2) {
+        if (colors.length == 2) { // for 2 colors: use the Java implementation
             // The hasAlpha flag in Color() is only used to enforce alpha to 0xFF if false.
             // If true the alpha is read from the int.
-            mGradientPaint = new GradientPaint(x0, y0, new Color(colors[0], true /* hasalpha */),
+            mJavaPaint = new GradientPaint(x0, y0, new Color(colors[0], true /* hasalpha */),
                     x1, y1, new Color(colors[1], true /* hasalpha */), tile != TileMode.CLAMP);
+        } else {
+            mJavaPaint = new MultiPointLinearGradientPaint(x0, y0, x1, y1, colors, positions, tile);
         }
     }
 
@@ -70,7 +81,7 @@
             TileMode tile) {
         // The hasAlpha flag in Color() is only used to enforce alpha to 0xFF if false.
         // If true the alpha is read from the int.
-        mGradientPaint = new GradientPaint(x0, y0, new Color(color0, true /* hasalpha */), x1, y1,
+        mJavaPaint = new GradientPaint(x0, y0, new Color(color0, true /* hasalpha */), x1, y1,
                 new Color(color1, true /* hasalpha */), tile != TileMode.CLAMP);
     }
 
@@ -78,6 +89,232 @@
 
     @Override
     public Paint getJavaPaint() {
-        return mGradientPaint;
+        return mJavaPaint;
+    }
+
+    private static class MultiPointLinearGradientPaint implements Paint {
+        private final static int GRADIENT_SIZE = 100;
+
+        private final float mX0;
+        private final float mY0;
+        private final float mDx;
+        private final float mDy;
+        private final float mDSize2;
+        private final int[] mColors;
+        private final float[] mPositions;
+        private final TileMode mTile;
+        private int[] mGradient;
+
+        public MultiPointLinearGradientPaint(float x0, float y0, float x1, float y1, int colors[],
+                float positions[], TileMode tile) {
+                mX0 = x0;
+                mY0 = y0;
+                mDx = x1 - x0;
+                mDy = y1 - y0;
+                mDSize2 = mDx * mDx + mDy * mDy;
+
+                mColors = colors;
+                mPositions = positions;
+                mTile = tile;
+        }
+
+        public PaintContext createContext(ColorModel cm, Rectangle deviceBounds,
+                Rectangle2D userBounds, AffineTransform xform, RenderingHints hints) {
+            prepareColors();
+            return new MultiPointLinearGradientPaintContext(cm, deviceBounds,
+                    userBounds, xform, hints);
+        }
+
+        public int getTransparency() {
+            return TRANSLUCENT;
+        }
+
+        private synchronized void prepareColors() {
+            if (mGradient == null) {
+                // actually create an array with an extra size, so that we can really go
+                // from 0 to SIZE (100%), or currentPos in the loop below will never equal 1.0
+                mGradient = new int[GRADIENT_SIZE+1];
+
+                int prevPos = 0;
+                int nextPos = 1;
+                for (int i  = 0 ; i <= GRADIENT_SIZE ; i++) {
+                    // compute current position
+                    float currentPos = (float)i/GRADIENT_SIZE;
+                    while (currentPos > mPositions[nextPos]) {
+                        prevPos = nextPos++;
+                    }
+
+                    float percent = (currentPos - mPositions[prevPos]) /
+                            (mPositions[nextPos] - mPositions[prevPos]);
+
+                    mGradient[i] = getColor(mColors[prevPos], mColors[nextPos], percent);
+                }
+            }
+        }
+
+        /**
+         * Returns the color between c1, and c2, based on the percent of the distance
+         * between c1 and c2.
+         */
+        private int getColor(int c1, int c2, float percent) {
+            int a = getChannel((c1 >> 24) & 0xFF, (c2 >> 24) & 0xFF, percent);
+            int r = getChannel((c1 >> 16) & 0xFF, (c2 >> 16) & 0xFF, percent);
+            int g = getChannel((c1 >>  8) & 0xFF, (c2 >>  8) & 0xFF, percent);
+            int b = getChannel((c1      ) & 0xFF, (c2      ) & 0xFF, percent);
+            return a << 24 | r << 16 | g << 8 | b;
+        }
+
+        /**
+         * Returns the channel value between 2 values based on the percent of the distance between
+         * the 2 values..
+         */
+        private int getChannel(int c1, int c2, float percent) {
+            return c1 + (int)((percent * (c2-c1)) + .5);
+        }
+
+        private class MultiPointLinearGradientPaintContext implements PaintContext {
+
+            private ColorModel mColorModel;
+            private final Rectangle mDeviceBounds;
+            private final Rectangle2D mUserBounds;
+            private final AffineTransform mXform;
+            private final RenderingHints mHints;
+
+            public MultiPointLinearGradientPaintContext(ColorModel cm, Rectangle deviceBounds,
+                    Rectangle2D userBounds, AffineTransform xform, RenderingHints hints) {
+                mColorModel = cm;
+                // FIXME: so far all this is always the same rect gotten in getRaster with an indentity matrix?
+                mDeviceBounds = deviceBounds;
+                mUserBounds = userBounds;
+                mXform = xform;
+                mHints = hints;
+            }
+
+            public void dispose() {
+            }
+
+            public ColorModel getColorModel() {
+                return mColorModel;
+            }
+
+            public Raster getRaster(int x, int y, int w, int h) {
+                SampleModel sampleModel = mColorModel.createCompatibleSampleModel(w, h);
+                WritableRaster raster = Raster.createWritableRaster(sampleModel,
+                        new java.awt.Point(x, y));
+
+                DataBuffer data = raster.getDataBuffer();
+
+                if (mDx == 0) { // vertical gradient
+                    // compute first column and copy to all other columns
+                    int index = 0;
+                    for (int iy = 0 ; iy < h ; iy++) {
+                        int color = getColor(iy + y, mY0, mDy);
+                        for (int ix = 0 ; ix < w ; ix++) {
+                            data.setElem(index++, color);
+                        }
+                    }
+                } else if (mDy == 0) { // horizontal
+                    // compute first line in a tmp array and copy to all lines
+                    int[] line = new int[w];
+                    for (int ix = 0 ; ix < w ; ix++) {
+                        line[ix] = getColor(ix + x, mX0, mDx);
+                    }
+
+                    int index = 0;
+                    for (int iy = 0 ; iy < h ; iy++) {
+                        for (int ix = 0 ; ix < w ; ix++) {
+                            data.setElem(index++, line[ix]);
+                        }
+                    }
+                } else {
+                    int index = 0;
+                    for (int iy = 0 ; iy < h ; iy++) {
+                        for (int ix = 0 ; ix < w ; ix++) {
+                            data.setElem(index++, getColor(ix + x, iy + y));
+                        }
+                    }
+                }
+
+                return raster;
+            }
+        }
+
+        /** Returns a color for the easy vertical/horizontal mode */
+        private int getColor(float absPos, float refPos, float refSize) {
+            float pos = (absPos - refPos) / refSize;
+
+            return getIndexFromPos(pos);
+        }
+
+        /**
+         * Returns a color for an arbitrary point.
+         */
+        private int getColor(float x, float y) {
+            // find the x position on the gradient vector.
+            float _x = (mDx*mDy*(y-mY0) + mDy*mDy*mX0 + mDx*mDx*x) / mDSize2;
+            // from it get the position relative to the vector
+            float pos = (float) ((_x - mX0) / mDx);
+
+            return getIndexFromPos(pos);
+        }
+
+        /**
+         * Returns the color based on the position in the gradient.
+         * <var>pos</var> can be anything, even &lt; 0 or &gt; > 1, as the gradient
+         * will use {@link TileMode} value to convert it into a [0,1] value.
+         */
+        private int getIndexFromPos(float pos) {
+            if (pos < 0.f) {
+                switch (mTile) {
+                    case CLAMP:
+                        pos = 0.f;
+                        break;
+                    case REPEAT:
+                        // remove the integer part to stay in the [0,1] range
+                        // careful: this is a negative value, so use ceil instead of floor
+                        pos = pos - (float)Math.ceil(pos);
+                        break;
+                    case MIRROR:
+                        // get the integer and the decimal part
+                        // careful: this is a negative value, so use ceil instead of floor
+                        int intPart = (int)Math.ceil(pos);
+                        pos = pos - intPart;
+                        // 0  -> -1 : mirrored order
+                        // -1 -> -2: normal order
+                        // etc..
+                        // this means if the intpart is even we invert
+                        if ((intPart % 2) == 0) {
+                            pos = 1.f - pos;
+                        }
+                        break;
+                }
+            } else if (pos > 1f) {
+                switch (mTile) {
+                    case CLAMP:
+                        pos = 1.f;
+                        break;
+                    case REPEAT:
+                        // remove the integer part to stay in the [0,1] range
+                        pos = pos - (float)Math.floor(pos);
+                        break;
+                    case MIRROR:
+                        // get the integer and the decimal part
+                        int intPart = (int)Math.floor(pos);
+                        pos = pos - intPart;
+                        // 0 -> 1 : normal order
+                        // 1 -> 2: mirrored
+                        // etc..
+                        // this means if the intpart is odd we invert
+                        if ((intPart % 2) == 1) {
+                            pos = 1.f - pos;
+                        }
+                        break;
+                }
+            }
+
+            int index = (int)((pos * GRADIENT_SIZE) + .5);
+
+            return mGradient[index];
+        }
     }
 }